diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ccdafb --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +# Editor temporary/working/backup files # +######################################### +.#* +[#]*# +*~ +*$ +*.bak +*.diff +.idea/ +*.iml +*.ipr +*.iws +*.org +.project +pmip +*.rej +.settings/ +.*.sw[nop] +.sw[nop] +*.tmp +*.vim +.vscode +tags +cscope.out +# gnu global +GPATH +GRTAGS +GSYMS +GTAGS +.cache + +# Compiled source # +################### +*.a +*.com +*.class +*.dll +*.exe +*.o +*.o.d +*.py[ocd] +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.bz2 +*.bzip2 +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.tbz2 +*.tgz +*.zip + +# Python files # +################ +# setup.py working directory +build +# sphinx build directory +_build +# setup.py dist directory +dist +doc/build +doc/cdoc/build +# Egg metadata +*.egg-info +# The shelf plugin uses this dir +./.shelf +MANIFEST +.cache + +# Paver generated files # +######################### +/release + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# Patches # +########### +*.patch +*.diff + +# OS generated files # +###################### +.DS_Store* +.VolumeIcon.icns +.fseventsd +Icon? +.gdb_history +ehthumbs.db +Thumbs.db +.directory + +# pytest generated files # +########################## +/.pytest_cache + +# Things specific to this project # +################################### diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5eae320 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,30 @@ +Copyright (c) 2005-2019, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/numpy_financial/__init__.py b/numpy_financial/__init__.py new file mode 100644 index 0000000..49a2377 --- /dev/null +++ b/numpy_financial/__init__.py @@ -0,0 +1,3 @@ + + +from ._financial import * diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py new file mode 100644 index 0000000..d72384e --- /dev/null +++ b/numpy_financial/_financial.py @@ -0,0 +1,851 @@ +"""Some simple financial calculations + +patterned after spreadsheet computations. + +There is some complexity in each function +so that the functions behave like ufuncs with +broadcasting and being able to be called with scalars +or arrays (or other sequences). + +Functions support the :class:`decimal.Decimal` type unless +otherwise stated. +""" +from __future__ import division, absolute_import, print_function + +from decimal import Decimal +import functools + +import numpy as np +from numpy.core import overrides + + +array_function_dispatch = functools.partial( + overrides.array_function_dispatch, module='numpy') + + +__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate', + 'irr', 'npv', 'mirr'] + +_when_to_num = {'end':0, 'begin':1, + 'e':0, 'b':1, + 0:0, 1:1, + 'beginning':1, + 'start':1, + 'finish':0} + +def _convert_when(when): + #Test to see if when has already been converted to ndarray + #This will happen if one function calls another, for example ppmt + if isinstance(when, np.ndarray): + return when + try: + return _when_to_num[when] + except (KeyError, TypeError): + return [_when_to_num[x] for x in when] + + +def _fv_dispatcher(rate, nper, pmt, pv, when=None): + return (rate, nper, pmt, pv) + + +@array_function_dispatch(_fv_dispatcher) +def fv(rate, nper, pmt, pv, when='end'): + """ + Compute the future value. + + Given: + * a present value, `pv` + * an interest `rate` compounded once per period, of which + there are + * `nper` total + * a (fixed) payment, `pmt`, paid either + * at the beginning (`when` = {'begin', 1}) or the end + (`when` = {'end', 0}) of each period + + Return: + the value at the end of the `nper` periods + + Parameters + ---------- + rate : scalar or array_like of shape(M, ) + Rate of interest as decimal (not per cent) per period + nper : scalar or array_like of shape(M, ) + Number of compounding periods + pmt : scalar or array_like of shape(M, ) + Payment + pv : scalar or array_like of shape(M, ) + Present value + when : {{'begin', 1}, {'end', 0}}, {string, int}, optional + When payments are due ('begin' (1) or 'end' (0)). + Defaults to {'end', 0}. + + Returns + ------- + out : ndarray + Future values. If all input is scalar, returns a scalar float. If + any input is array_like, returns future values for each input element. + If multiple inputs are array_like, they all must have the same shape. + + Notes + ----- + The future value is computed by solving the equation:: + + fv + + pv*(1+rate)**nper + + pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0 + + or, when ``rate == 0``:: + + fv + pv + pmt * nper == 0 + + References + ---------- + .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). + Open Document Format for Office Applications (OpenDocument)v1.2, + Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, + Pre-Draft 12. Organization for the Advancement of Structured Information + Standards (OASIS). Billerica, MA, USA. [ODT Document]. + Available: + http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula + OpenDocument-formula-20090508.odt + + Examples + -------- + What is the future value after 10 years of saving $100 now, with + an additional monthly savings of $100. Assume the interest rate is + 5% (annually) compounded monthly? + + >>> np.fv(0.05/12, 10*12, -100, -100) + 15692.928894335748 + + By convention, the negative sign represents cash flow out (i.e. money not + available today). Thus, saving $100 a month at 5% annual interest leads + to $15,692.93 available to spend in 10 years. + + If any input is array_like, returns an array of equal shape. Let's + compare different interest rates from the example above. + + >>> a = np.array((0.05, 0.06, 0.07))/12 + >>> np.fv(a, 10*12, -100, -100) + array([ 15692.92889434, 16569.87435405, 17509.44688102]) # may vary + + """ + when = _convert_when(when) + (rate, nper, pmt, pv, when) = map(np.asarray, [rate, nper, pmt, pv, when]) + temp = (1+rate)**nper + fact = np.where(rate == 0, nper, + (1 + rate*when)*(temp - 1)/rate) + return -(pv*temp + pmt*fact) + + +def _pmt_dispatcher(rate, nper, pv, fv=None, when=None): + return (rate, nper, pv, fv) + + +@array_function_dispatch(_pmt_dispatcher) +def pmt(rate, nper, pv, fv=0, when='end'): + """ + Compute the payment against loan principal plus interest. + + Given: + * a present value, `pv` (e.g., an amount borrowed) + * a future value, `fv` (e.g., 0) + * an interest `rate` compounded once per period, of which + there are + * `nper` total + * and (optional) specification of whether payment is made + at the beginning (`when` = {'begin', 1}) or the end + (`when` = {'end', 0}) of each period + + Return: + the (fixed) periodic payment. + + Parameters + ---------- + rate : array_like + Rate of interest (per period) + nper : array_like + Number of compounding periods + pv : array_like + Present value + fv : array_like, optional + Future value (default = 0) + when : {{'begin', 1}, {'end', 0}}, {string, int} + When payments are due ('begin' (1) or 'end' (0)) + + Returns + ------- + out : ndarray + Payment against loan plus interest. If all input is scalar, returns a + scalar float. If any input is array_like, returns payment for each + input element. If multiple inputs are array_like, they all must have + the same shape. + + Notes + ----- + The payment is computed by solving the equation:: + + fv + + pv*(1 + rate)**nper + + pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0 + + or, when ``rate == 0``:: + + fv + pv + pmt * nper == 0 + + for ``pmt``. + + Note that computing a monthly mortgage payment is only + one use for this function. For example, pmt returns the + periodic deposit one must make to achieve a specified + future balance given an initial deposit, a fixed, + periodically compounded interest rate, and the total + number of periods. + + References + ---------- + .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). + Open Document Format for Office Applications (OpenDocument)v1.2, + Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, + Pre-Draft 12. Organization for the Advancement of Structured Information + Standards (OASIS). Billerica, MA, USA. [ODT Document]. + Available: + http://www.oasis-open.org/committees/documents.php + ?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt + + Examples + -------- + What is the monthly payment needed to pay off a $200,000 loan in 15 + years at an annual interest rate of 7.5%? + + >>> np.pmt(0.075/12, 12*15, 200000) + -1854.0247200054619 + + In order to pay-off (i.e., have a future-value of 0) the $200,000 obtained + today, a monthly payment of $1,854.02 would be required. Note that this + example illustrates usage of `fv` having a default value of 0. + + """ + when = _convert_when(when) + (rate, nper, pv, fv, when) = map(np.array, [rate, nper, pv, fv, when]) + temp = (1 + rate)**nper + mask = (rate == 0) + masked_rate = np.where(mask, 1, rate) + fact = np.where(mask != 0, nper, + (1 + masked_rate*when)*(temp - 1)/masked_rate) + return -(fv + pv*temp) / fact + + +def _nper_dispatcher(rate, pmt, pv, fv=None, when=None): + return (rate, pmt, pv, fv) + + +@array_function_dispatch(_nper_dispatcher) +def nper(rate, pmt, pv, fv=0, when='end'): + """ + Compute the number of periodic payments. + + :class:`decimal.Decimal` type is not supported. + + Parameters + ---------- + rate : array_like + Rate of interest (per period) + pmt : array_like + Payment + pv : array_like + Present value + fv : array_like, optional + Future value + when : {{'begin', 1}, {'end', 0}}, {string, int}, optional + When payments are due ('begin' (1) or 'end' (0)) + + Notes + ----- + The number of periods ``nper`` is computed by solving the equation:: + + fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate*((1+rate)**nper-1) = 0 + + but if ``rate = 0`` then:: + + fv + pv + pmt*nper = 0 + + Examples + -------- + If you only had $150/month to pay towards the loan, how long would it take + to pay-off a loan of $8,000 at 7% annual interest? + + >>> print(np.round(np.nper(0.07/12, -150, 8000), 5)) + 64.07335 + + So, over 64 months would be required to pay off the loan. + + The same analysis could be done with several different interest rates + and/or payments and/or total amounts to produce an entire table. + + >>> np.nper(*(np.ogrid[0.07/12: 0.08/12: 0.01/12, + ... -150 : -99 : 50 , + ... 8000 : 9001 : 1000])) + array([[[ 64.07334877, 74.06368256], + [108.07548412, 127.99022654]], + [[ 66.12443902, 76.87897353], + [114.70165583, 137.90124779]]]) + + """ + when = _convert_when(when) + (rate, pmt, pv, fv, when) = map(np.asarray, [rate, pmt, pv, fv, when]) + + use_zero_rate = False + with np.errstate(divide="raise"): + try: + z = pmt*(1+rate*when)/rate + except FloatingPointError: + use_zero_rate = True + + if use_zero_rate: + return (-fv + pv) / pmt + else: + A = -(fv + pv)/(pmt+0) + B = np.log((-fv+z) / (pv+z))/np.log(1+rate) + return np.where(rate == 0, A, B) + + +def _ipmt_dispatcher(rate, per, nper, pv, fv=None, when=None): + return (rate, per, nper, pv, fv) + + +@array_function_dispatch(_ipmt_dispatcher) +def ipmt(rate, per, nper, pv, fv=0, when='end'): + """ + Compute the interest portion of a payment. + + Parameters + ---------- + rate : scalar or array_like of shape(M, ) + Rate of interest as decimal (not per cent) per period + per : scalar or array_like of shape(M, ) + Interest paid against the loan changes during the life or the loan. + The `per` is the payment period to calculate the interest amount. + nper : scalar or array_like of shape(M, ) + Number of compounding periods + pv : scalar or array_like of shape(M, ) + Present value + fv : scalar or array_like of shape(M, ), optional + Future value + when : {{'begin', 1}, {'end', 0}}, {string, int}, optional + When payments are due ('begin' (1) or 'end' (0)). + Defaults to {'end', 0}. + + Returns + ------- + out : ndarray + Interest portion of payment. If all input is scalar, returns a scalar + float. If any input is array_like, returns interest payment for each + input element. If multiple inputs are array_like, they all must have + the same shape. + + See Also + -------- + ppmt, pmt, pv + + Notes + ----- + The total payment is made up of payment against principal plus interest. + + ``pmt = ppmt + ipmt`` + + Examples + -------- + What is the amortization schedule for a 1 year loan of $2500 at + 8.24% interest per year compounded monthly? + + >>> principal = 2500.00 + + The 'per' variable represents the periods of the loan. Remember that + financial equations start the period count at 1! + + >>> per = np.arange(1*12) + 1 + >>> ipmt = np.ipmt(0.0824/12, per, 1*12, principal) + >>> ppmt = np.ppmt(0.0824/12, per, 1*12, principal) + + Each element of the sum of the 'ipmt' and 'ppmt' arrays should equal + 'pmt'. + + >>> pmt = np.pmt(0.0824/12, 1*12, principal) + >>> np.allclose(ipmt + ppmt, pmt) + True + + >>> fmt = '{0:2d} {1:8.2f} {2:8.2f} {3:8.2f}' + >>> for payment in per: + ... index = payment - 1 + ... principal = principal + ppmt[index] + ... print(fmt.format(payment, ppmt[index], ipmt[index], principal)) + 1 -200.58 -17.17 2299.42 + 2 -201.96 -15.79 2097.46 + 3 -203.35 -14.40 1894.11 + 4 -204.74 -13.01 1689.37 + 5 -206.15 -11.60 1483.22 + 6 -207.56 -10.18 1275.66 + 7 -208.99 -8.76 1066.67 + 8 -210.42 -7.32 856.25 + 9 -211.87 -5.88 644.38 + 10 -213.32 -4.42 431.05 + 11 -214.79 -2.96 216.26 + 12 -216.26 -1.49 -0.00 + + >>> interestpd = np.sum(ipmt) + >>> np.round(interestpd, 2) + -112.98 + + """ + when = _convert_when(when) + rate, per, nper, pv, fv, when = np.broadcast_arrays(rate, per, nper, + pv, fv, when) + total_pmt = pmt(rate, nper, pv, fv, when) + ipmt = _rbl(rate, per, total_pmt, pv, when)*rate + try: + ipmt = np.where(when == 1, ipmt/(1 + rate), ipmt) + ipmt = np.where(np.logical_and(when == 1, per == 1), 0, ipmt) + except IndexError: + pass + return ipmt + + +def _rbl(rate, per, pmt, pv, when): + """ + This function is here to simply have a different name for the 'fv' + function to not interfere with the 'fv' keyword argument within the 'ipmt' + function. It is the 'remaining balance on loan' which might be useful as + it's own function, but is easily calculated with the 'fv' function. + """ + return fv(rate, (per - 1), pmt, pv, when) + + +def _ppmt_dispatcher(rate, per, nper, pv, fv=None, when=None): + return (rate, per, nper, pv, fv) + + +@array_function_dispatch(_ppmt_dispatcher) +def ppmt(rate, per, nper, pv, fv=0, when='end'): + """ + Compute the payment against loan principal. + + Parameters + ---------- + rate : array_like + Rate of interest (per period) + per : array_like, int + Amount paid against the loan changes. The `per` is the period of + interest. + nper : array_like + Number of compounding periods + pv : array_like + Present value + fv : array_like, optional + Future value + when : {{'begin', 1}, {'end', 0}}, {string, int} + When payments are due ('begin' (1) or 'end' (0)) + + See Also + -------- + pmt, pv, ipmt + + """ + total = pmt(rate, nper, pv, fv, when) + return total - ipmt(rate, per, nper, pv, fv, when) + + +def _pv_dispatcher(rate, nper, pmt, fv=None, when=None): + return (rate, nper, nper, pv, fv) + + +@array_function_dispatch(_pv_dispatcher) +def pv(rate, nper, pmt, fv=0, when='end'): + """ + Compute the present value. + + Given: + * a future value, `fv` + * an interest `rate` compounded once per period, of which + there are + * `nper` total + * a (fixed) payment, `pmt`, paid either + * at the beginning (`when` = {'begin', 1}) or the end + (`when` = {'end', 0}) of each period + + Return: + the value now + + Parameters + ---------- + rate : array_like + Rate of interest (per period) + nper : array_like + Number of compounding periods + pmt : array_like + Payment + fv : array_like, optional + Future value + when : {{'begin', 1}, {'end', 0}}, {string, int}, optional + When payments are due ('begin' (1) or 'end' (0)) + + Returns + ------- + out : ndarray, float + Present value of a series of payments or investments. + + Notes + ----- + The present value is computed by solving the equation:: + + fv + + pv*(1 + rate)**nper + + pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) = 0 + + or, when ``rate = 0``:: + + fv + pv + pmt * nper = 0 + + for `pv`, which is then returned. + + References + ---------- + .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). + Open Document Format for Office Applications (OpenDocument)v1.2, + Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, + Pre-Draft 12. Organization for the Advancement of Structured Information + Standards (OASIS). Billerica, MA, USA. [ODT Document]. + Available: + http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula + OpenDocument-formula-20090508.odt + + Examples + -------- + What is the present value (e.g., the initial investment) + of an investment that needs to total $15692.93 + after 10 years of saving $100 every month? Assume the + interest rate is 5% (annually) compounded monthly. + + >>> np.pv(0.05/12, 10*12, -100, 15692.93) + -100.00067131625819 + + By convention, the negative sign represents cash flow out + (i.e., money not available today). Thus, to end up with + $15,692.93 in 10 years saving $100 a month at 5% annual + interest, one's initial deposit should also be $100. + + If any input is array_like, ``pv`` returns an array of equal shape. + Let's compare different interest rates in the example above: + + >>> a = np.array((0.05, 0.04, 0.03))/12 + >>> np.pv(a, 10*12, -100, 15692.93) + array([ -100.00067132, -649.26771385, -1273.78633713]) # may vary + + So, to end up with the same $15692.93 under the same $100 per month + "savings plan," for annual interest rates of 4% and 3%, one would + need initial investments of $649.27 and $1273.79, respectively. + + """ + when = _convert_when(when) + (rate, nper, pmt, fv, when) = map(np.asarray, [rate, nper, pmt, fv, when]) + temp = (1+rate)**nper + fact = np.where(rate == 0, nper, (1+rate*when)*(temp-1)/rate) + return -(fv + pmt*fact)/temp + +# Computed with Sage +# (y + (r + 1)^n*x + p*((r + 1)^n - 1)*(r*w + 1)/r)/(n*(r + 1)^(n - 1)*x - +# p*((r + 1)^n - 1)*(r*w + 1)/r^2 + n*p*(r + 1)^(n - 1)*(r*w + 1)/r + +# p*((r + 1)^n - 1)*w/r) + +def _g_div_gp(r, n, p, x, y, w): + t1 = (r+1)**n + t2 = (r+1)**(n-1) + return ((y + t1*x + p*(t1 - 1)*(r*w + 1)/r) / + (n*t2*x - p*(t1 - 1)*(r*w + 1)/(r**2) + n*p*t2*(r*w + 1)/r + + p*(t1 - 1)*w/r)) + + +def _rate_dispatcher(nper, pmt, pv, fv, when=None, guess=None, tol=None, + maxiter=None): + return (nper, pmt, pv, fv) + + +# Use Newton's iteration until the change is less than 1e-6 +# for all values or a maximum of 100 iterations is reached. +# Newton's rule is +# r_{n+1} = r_{n} - g(r_n)/g'(r_n) +# where +# g(r) is the formula +# g'(r) is the derivative with respect to r. +@array_function_dispatch(_rate_dispatcher) +def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100): + """ + Compute the rate of interest per period. + + Parameters + ---------- + nper : array_like + Number of compounding periods + pmt : array_like + Payment + pv : array_like + Present value + fv : array_like + Future value + when : {{'begin', 1}, {'end', 0}}, {string, int}, optional + When payments are due ('begin' (1) or 'end' (0)) + guess : Number, optional + Starting guess for solving the rate of interest, default 0.1 + tol : Number, optional + Required tolerance for the solution, default 1e-6 + maxiter : int, optional + Maximum iterations in finding the solution + + Notes + ----- + The rate of interest is computed by iteratively solving the + (non-linear) equation:: + + fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate * ((1+rate)**nper - 1) = 0 + + for ``rate``. + + References + ---------- + Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). Open Document + Format for Office Applications (OpenDocument)v1.2, Part 2: Recalculated + Formula (OpenFormula) Format - Annotated Version, Pre-Draft 12. + Organization for the Advancement of Structured Information Standards + (OASIS). Billerica, MA, USA. [ODT Document]. Available: + http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula + OpenDocument-formula-20090508.odt + + """ + when = _convert_when(when) + default_type = Decimal if isinstance(pmt, Decimal) else float + + # Handle casting defaults to Decimal if/when pmt is a Decimal and + # guess and/or tol are not given default values + if guess is None: + guess = default_type('0.1') + + if tol is None: + tol = default_type('1e-6') + + (nper, pmt, pv, fv, when) = map(np.asarray, [nper, pmt, pv, fv, when]) + + rn = guess + iterator = 0 + close = False + while (iterator < maxiter) and not close: + rnp1 = rn - _g_div_gp(rn, nper, pmt, pv, fv, when) + diff = abs(rnp1-rn) + close = np.all(diff < tol) + iterator += 1 + rn = rnp1 + if not close: + # Return nan's in array of the same shape as rn + return np.nan + rn + else: + return rn + + +def _irr_dispatcher(values): + return (values,) + + +@array_function_dispatch(_irr_dispatcher) +def irr(values): + """ + Return the Internal Rate of Return (IRR). + + This is the "average" periodically compounded rate of return + that gives a net present value of 0.0; for a more complete explanation, + see Notes below. + + :class:`decimal.Decimal` type is not supported. + + Parameters + ---------- + values : array_like, shape(N,) + Input cash flows per time period. By convention, net "deposits" + are negative and net "withdrawals" are positive. Thus, for + example, at least the first element of `values`, which represents + the initial investment, will typically be negative. + + Returns + ------- + out : float + Internal Rate of Return for periodic input values. + + Notes + ----- + The IRR is perhaps best understood through an example (illustrated + using np.irr in the Examples section below). Suppose one invests 100 + units and then makes the following withdrawals at regular (fixed) + intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100 + unit investment yields 173 units; however, due to the combination of + compounding and the periodic withdrawals, the "average" rate of return + is neither simply 0.73/4 nor (1.73)^0.25-1. Rather, it is the solution + (for :math:`r`) of the equation: + + .. math:: -100 + \\frac{39}{1+r} + \\frac{59}{(1+r)^2} + + \\frac{55}{(1+r)^3} + \\frac{20}{(1+r)^4} = 0 + + In general, for `values` :math:`= [v_0, v_1, ... v_M]`, + irr is the solution of the equation: [G]_ + + .. math:: \\sum_{t=0}^M{\\frac{v_t}{(1+irr)^{t}}} = 0 + + References + ---------- + .. [G] L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., + Addison-Wesley, 2003, pg. 348. + + Examples + -------- + >>> round(np.irr([-100, 39, 59, 55, 20]), 5) + 0.28095 + >>> round(np.irr([-100, 0, 0, 74]), 5) + -0.0955 + >>> round(np.irr([-100, 100, 0, -7]), 5) + -0.0833 + >>> round(np.irr([-100, 100, 0, 7]), 5) + 0.06206 + >>> round(np.irr([-5, 10.5, 1, -8, 1]), 5) + 0.0886 + + """ + # `np.roots` call is why this function does not support Decimal type. + # + # Ultimately Decimal support needs to be added to np.roots, which has + # greater implications on the entire linear algebra module and how it does + # eigenvalue computations. + res = np.roots(values[::-1]) + mask = (res.imag == 0) & (res.real > 0) + if not mask.any(): + return np.nan + res = res[mask].real + # NPV(rate) = 0 can have more than one solution so we return + # only the solution closest to zero. + rate = 1/res - 1 + rate = rate.item(np.argmin(np.abs(rate))) + return rate + + +def _npv_dispatcher(rate, values): + return (values,) + + +@array_function_dispatch(_npv_dispatcher) +def npv(rate, values): + """ + Returns the NPV (Net Present Value) of a cash flow series. + + Parameters + ---------- + rate : scalar + The discount rate. + values : array_like, shape(M, ) + The values of the time series of cash flows. The (fixed) time + interval between cash flow "events" must be the same as that for + which `rate` is given (i.e., if `rate` is per year, then precisely + a year is understood to elapse between each cash flow event). By + convention, investments or "deposits" are negative, income or + "withdrawals" are positive; `values` must begin with the initial + investment, thus `values[0]` will typically be negative. + + Returns + ------- + out : float + The NPV of the input cash flow series `values` at the discount + `rate`. + + Warnings + -------- + ``npv`` considers a series of cashflows starting in the present (t = 0). + NPV can also be defined with a series of future cashflows, paid at the + end, rather than the start, of each period. If future cashflows are used, + the first cashflow `values[0]` must be zeroed and added to the net + present value of the future cashflows. This is demonstrated in the + examples. + + Notes + ----- + Returns the result of: [G]_ + + .. math :: \\sum_{t=0}^{M-1}{\\frac{values_t}{(1+rate)^{t}}} + + References + ---------- + .. [G] L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., + Addison-Wesley, 2003, pg. 346. + + Examples + -------- + Consider a potential project with an initial investment of $40 000 and + projected cashflows of $5 000, $8 000, $12 000 and $30 000 at the end of + each period discounted at a rate of 8% per period. To find the project's + net present value: + + >>> rate, cashflows = 0.08, [-40_000, 5_000, 8_000, 12_000, 30_000] + >>> np.npv(rate, cashflows).round(5) + 3065.22267 + + It may be preferable to split the projected cashflow into an initial + investment and expected future cashflows. In this case, the value of + the initial cashflow is zero and the initial investment is later added + to the future cashflows net present value: + + >>> initial_cashflow = cashflows[0] + >>> cashflows[0] = 0 + >>> np.round(np.npv(rate, cashflows) + initial_cashflow, 5) + 3065.22267 + + """ + values = np.asarray(values) + return (values / (1+rate)**np.arange(0, len(values))).sum(axis=0) + + +def _mirr_dispatcher(values, finance_rate, reinvest_rate): + return (values,) + + +@array_function_dispatch(_mirr_dispatcher) +def mirr(values, finance_rate, reinvest_rate): + """ + Modified internal rate of return. + + Parameters + ---------- + values : array_like + Cash flows (must contain at least one positive and one negative + value) or nan is returned. The first value is considered a sunk + cost at time zero. + finance_rate : scalar + Interest rate paid on the cash flows + reinvest_rate : scalar + Interest rate received on the cash flows upon reinvestment + + Returns + ------- + out : float + Modified internal rate of return + + """ + values = np.asarray(values) + n = values.size + + # Without this explicit cast the 1/(n - 1) computation below + # becomes a float, which causes TypeError when using Decimal + # values. + if isinstance(finance_rate, Decimal): + n = Decimal(n) + + pos = values > 0 + neg = values < 0 + if not (pos.any() and neg.any()): + return np.nan + numer = np.abs(npv(reinvest_rate, values*pos)) + denom = np.abs(npv(finance_rate, values*neg)) + return (numer/denom)**(1/(n - 1))*(1 + reinvest_rate) - 1 diff --git a/numpy_financial/tests/test_financial.py b/numpy_financial/tests/test_financial.py new file mode 100644 index 0000000..935b6ce --- /dev/null +++ b/numpy_financial/tests/test_financial.py @@ -0,0 +1,412 @@ + +from decimal import Decimal + +# Don't use 'import numpy as np', to avoid accidentally testing +# the versions in numpy instead of numpy_financial. +import numpy +from numpy.testing import ( + assert_, assert_almost_equal, assert_allclose, assert_equal, assert_raises + ) + +import numpy_financial as npf + + +class TestFinancial(object): + def test_npv_irr_congruence(self): + # IRR is defined as the rate required for the present value of a + # a series of cashflows to be zero i.e. NPV(IRR(x), x) = 0 + cashflows = numpy.array([-40000, 5000, 8000, 12000, 30000]) + assert_allclose(npf.npv(npf.irr(cashflows), cashflows), 0, + atol=1e-10, rtol=0) + + def test_rate(self): + assert_almost_equal(npf.rate(10, 0, -3500, 10000), 0.1107, 4) + + def test_rate_decimal(self): + rate = npf.rate(Decimal('10'), Decimal('0'), Decimal('-3500'), + Decimal('10000')) + assert_equal(Decimal('0.1106908537142689284704528100'), rate) + + def test_irr(self): + v = [-150000, 15000, 25000, 35000, 45000, 60000] + assert_almost_equal(npf.irr(v), 0.0524, 2) + v = [-100, 0, 0, 74] + assert_almost_equal(npf.irr(v), -0.0955, 2) + v = [-100, 39, 59, 55, 20] + assert_almost_equal(npf.irr(v), 0.28095, 2) + v = [-100, 100, 0, -7] + assert_almost_equal(npf.irr(v), -0.0833, 2) + v = [-100, 100, 0, 7] + assert_almost_equal(npf.irr(v), 0.06206, 2) + v = [-5, 10.5, 1, -8, 1] + assert_almost_equal(npf.irr(v), 0.0886, 2) + + # Test that if there is no solution then npf.irr returns nan + # Fixes gh-6744 + v = [-1, -2, -3] + assert_equal(npf.irr(v), numpy.nan) + + def test_pv(self): + assert_almost_equal(npf.pv(0.07, 20, 12000, 0), -127128.17, 2) + + def test_pv_decimal(self): + assert_equal(npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0')), + Decimal('-127128.1709461939327295222005')) + + def test_fv(self): + assert_equal(npf.fv(0.075, 20, -2000, 0, 0), 86609.362673042924) + + def test_fv_decimal(self): + assert_equal(npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + 0, 0), + Decimal('86609.36267304300040536731624')) + + def test_pmt(self): + res = npf.pmt(0.08 / 12, 5 * 12, 15000) + tgt = -304.145914 + assert_allclose(res, tgt) + # Test the edge case where rate == 0.0 + res = npf.pmt(0.0, 5 * 12, 15000) + tgt = -250.0 + assert_allclose(res, tgt) + # Test the case where we use broadcast and + # the arguments passed in are arrays. + res = npf.pmt([[0.0, 0.8], [0.3, 0.8]], [12, 3], [2000, 20000]) + tgt = numpy.array([[-166.66667, -19311.258], [-626.90814, -19311.258]]) + assert_allclose(res, tgt) + + def test_pmt_decimal(self): + res = npf.pmt(Decimal('0.08') / Decimal('12'), 5 * 12, 15000) + tgt = Decimal('-304.1459143262052370338701494') + assert_equal(res, tgt) + # Test the edge case where rate == 0.0 + res = npf.pmt(Decimal('0'), Decimal('60'), Decimal('15000')) + tgt = -250 + assert_equal(res, tgt) + + # Test the case where we use broadcast and + # the arguments passed in are arrays. + res = npf.pmt([[Decimal('0'), Decimal('0.8')], + [Decimal('0.3'), Decimal('0.8')]], + [Decimal('12'), Decimal('3')], + [Decimal('2000'), Decimal('20000')]) + tgt = numpy.array([[Decimal('-166.6666666666666666666666667'), + Decimal('-19311.25827814569536423841060')], + [Decimal('-626.9081401700757748402586600'), + Decimal('-19311.25827814569536423841060')]]) + + # Cannot use the `assert_allclose` because it uses isfinite under + # the covers which does not support the Decimal type + # See issue: https://github.com/numpy/numpy/issues/9954 + assert_equal(res[0][0], tgt[0][0]) + assert_equal(res[0][1], tgt[0][1]) + assert_equal(res[1][0], tgt[1][0]) + assert_equal(res[1][1], tgt[1][1]) + + def test_ppmt(self): + assert_equal(numpy.round(npf.ppmt(0.1 / 12, 1, 60, 55000), 2), -710.25) + + def test_ppmt_decimal(self): + assert_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000')), + Decimal('-710.2541257864217612489830917')) + + # Two tests showing how Decimal is actually getting at a more exact result + # .23 / 12 does not come out nicely as a float but does as a decimal + def test_ppmt_special_rate(self): + assert_equal(numpy.round(npf.ppmt(0.23 / 12, 1, 60, 10000000000), 8), + -90238044.232277036) + + def test_ppmt_special_rate_decimal(self): + # When rounded out to 8 decimal places like the float based test, + # this should not equal the same value as the float, substituted + # for the decimal + def raise_error_because_not_equal(): + assert_equal( + round(npf.ppmt(Decimal('0.23') / Decimal('12'), 1, 60, + Decimal('10000000000')), 8), + Decimal('-90238044.232277036')) + + assert_raises(AssertionError, raise_error_because_not_equal) + assert_equal(npf.ppmt(Decimal('0.23') / Decimal('12'), 1, 60, + Decimal('10000000000')), + Decimal('-90238044.2322778884413969909')) + + def test_ipmt(self): + assert_almost_equal(numpy.round(npf.ipmt(0.1 / 12, 1, 24, 2000), 2), + -16.67) + + def test_ipmt_decimal(self): + result = npf.ipmt(Decimal('0.1') / Decimal('12'), 1, 24, 2000) + assert_equal(result.flat[0], Decimal('-16.66666666666666666666666667')) + + def test_nper(self): + assert_almost_equal(npf.nper(0.075, -2000, 0, 100000.), + 21.54, 2) + + def test_nper2(self): + assert_almost_equal(npf.nper(0.0, -2000, 0, 100000.), + 50.0, 1) + + def test_npv(self): + assert_almost_equal( + npf.npv(0.05, [-15000, 1500, 2500, 3500, 4500, 6000]), + 122.89, 2) + + def test_npv_decimal(self): + assert_equal( + npf.npv(Decimal('0.05'), [-15000, 1500, 2500, 3500, 4500, 6000]), + Decimal('122.894854950942692161628715')) + + def test_mirr(self): + val = [-4500, -800, 800, 800, 600, 600, 800, 800, 700, 3000] + assert_almost_equal(npf.mirr(val, 0.08, 0.055), 0.0666, 4) + + val = [-120000, 39000, 30000, 21000, 37000, 46000] + assert_almost_equal(npf.mirr(val, 0.10, 0.12), 0.126094, 6) + + val = [100, 200, -50, 300, -200] + assert_almost_equal(npf.mirr(val, 0.05, 0.06), 0.3428, 4) + + val = [39000, 30000, 21000, 37000, 46000] + assert_(numpy.isnan(npf.mirr(val, 0.10, 0.12))) + + def test_mirr_decimal(self): + val = [Decimal('-4500'), Decimal('-800'), Decimal('800'), + Decimal('800'), Decimal('600'), Decimal('600'), Decimal('800'), + Decimal('800'), Decimal('700'), Decimal('3000')] + assert_equal(npf.mirr(val, Decimal('0.08'), Decimal('0.055')), + Decimal('0.066597175031553548874239618')) + + val = [Decimal('-120000'), Decimal('39000'), Decimal('30000'), + Decimal('21000'), Decimal('37000'), Decimal('46000')] + assert_equal(npf.mirr(val, Decimal('0.10'), Decimal('0.12')), + Decimal('0.126094130365905145828421880')) + + val = [Decimal('100'), Decimal('200'), Decimal('-50'), + Decimal('300'), Decimal('-200')] + assert_equal(npf.mirr(val, Decimal('0.05'), Decimal('0.06')), + Decimal('0.342823387842176663647819868')) + + val = [Decimal('39000'), Decimal('30000'), Decimal('21000'), + Decimal('37000'), Decimal('46000')] + assert_(numpy.isnan(npf.mirr(val, Decimal('0.10'), Decimal('0.12')))) + + def test_when(self): + # begin + assert_equal(npf.rate(10, 20, -3500, 10000, 1), + npf.rate(10, 20, -3500, 10000, 'begin')) + # end + assert_equal(npf.rate(10, 20, -3500, 10000), + npf.rate(10, 20, -3500, 10000, 'end')) + assert_equal(npf.rate(10, 20, -3500, 10000, 0), + npf.rate(10, 20, -3500, 10000, 'end')) + + # begin + assert_equal(npf.pv(0.07, 20, 12000, 0, 1), + npf.pv(0.07, 20, 12000, 0, 'begin')) + # end + assert_equal(npf.pv(0.07, 20, 12000, 0), + npf.pv(0.07, 20, 12000, 0, 'end')) + assert_equal(npf.pv(0.07, 20, 12000, 0, 0), + npf.pv(0.07, 20, 12000, 0, 'end')) + + # begin + assert_equal(npf.fv(0.075, 20, -2000, 0, 1), + npf.fv(0.075, 20, -2000, 0, 'begin')) + # end + assert_equal(npf.fv(0.075, 20, -2000, 0), + npf.fv(0.075, 20, -2000, 0, 'end')) + assert_equal(npf.fv(0.075, 20, -2000, 0, 0), + npf.fv(0.075, 20, -2000, 0, 'end')) + + # begin + assert_equal(npf.pmt(0.08 / 12, 5 * 12, 15000., 0, 1), + npf.pmt(0.08 / 12, 5 * 12, 15000., 0, 'begin')) + # end + assert_equal(npf.pmt(0.08 / 12, 5 * 12, 15000., 0), + npf.pmt(0.08 / 12, 5 * 12, 15000., 0, 'end')) + assert_equal(npf.pmt(0.08 / 12, 5 * 12, 15000., 0, 0), + npf.pmt(0.08 / 12, 5 * 12, 15000., 0, 'end')) + + # begin + assert_equal(npf.ppmt(0.1 / 12, 1, 60, 55000, 0, 1), + npf.ppmt(0.1 / 12, 1, 60, 55000, 0, 'begin')) + # end + assert_equal(npf.ppmt(0.1 / 12, 1, 60, 55000, 0), + npf.ppmt(0.1 / 12, 1, 60, 55000, 0, 'end')) + assert_equal(npf.ppmt(0.1 / 12, 1, 60, 55000, 0, 0), + npf.ppmt(0.1 / 12, 1, 60, 55000, 0, 'end')) + + # begin + assert_equal(npf.ipmt(0.1 / 12, 1, 24, 2000, 0, 1), + npf.ipmt(0.1 / 12, 1, 24, 2000, 0, 'begin')) + # end + assert_equal(npf.ipmt(0.1 / 12, 1, 24, 2000, 0), + npf.ipmt(0.1 / 12, 1, 24, 2000, 0, 'end')) + assert_equal(npf.ipmt(0.1 / 12, 1, 24, 2000, 0, 0), + npf.ipmt(0.1 / 12, 1, 24, 2000, 0, 'end')) + + # begin + assert_equal(npf.nper(0.075, -2000, 0, 100000., 1), + npf.nper(0.075, -2000, 0, 100000., 'begin')) + # end + assert_equal(npf.nper(0.075, -2000, 0, 100000.), + npf.nper(0.075, -2000, 0, 100000., 'end')) + assert_equal(npf.nper(0.075, -2000, 0, 100000., 0), + npf.nper(0.075, -2000, 0, 100000., 'end')) + + def test_decimal_with_when(self): + """ + Test that decimals are still supported if the when argument is passed + """ + # begin + assert_equal(npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000'), Decimal('1')), + npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000'), 'begin')) + # end + assert_equal(npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000')), + npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000'), 'end')) + assert_equal(npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000'), Decimal('0')), + npf.rate(Decimal('10'), Decimal('20'), Decimal('-3500'), + Decimal('10000'), 'end')) + + # begin + assert_equal(npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0'), Decimal('1')), + npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0'), 'begin')) + # end + assert_equal(npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0')), + npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0'), 'end')) + assert_equal(npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0'), Decimal('0')), + npf.pv(Decimal('0.07'), Decimal('20'), Decimal('12000'), + Decimal('0'), 'end')) + + # begin + assert_equal(npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0'), Decimal('1')), + npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0'), 'begin')) + # end + assert_equal(npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0')), + npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0'), 'end')) + assert_equal(npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0'), Decimal('0')), + npf.fv(Decimal('0.075'), Decimal('20'), Decimal('-2000'), + Decimal('0'), 'end')) + + # begin + assert_equal(npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0'), Decimal('1')), + npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0'), 'begin')) + # end + assert_equal(npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0')), + npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0'), 'end')) + assert_equal(npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0'), Decimal('0')), + npf.pmt(Decimal('0.08') / Decimal('12'), + Decimal('5') * Decimal('12'), Decimal('15000.'), + Decimal('0'), 'end')) + + # begin + assert_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), + Decimal('1'), Decimal('60'), Decimal('55000'), + Decimal('0'), Decimal('1')), + npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000'), + Decimal('0'), 'begin')) + # end + assert_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000'), Decimal('0')), + npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000'), Decimal('0'), + 'end')) + assert_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000'), Decimal('0'), + Decimal('0')), + npf.ppmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('60'), Decimal('55000'), Decimal('0'), + 'end')) + + # begin + assert_equal(npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0'), Decimal('1')).flat[0], + npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0'), 'begin').flat[0]) + # end + assert_equal(npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0')).flat[0], + npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0'), 'end').flat[0]) + assert_equal(npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0'), Decimal('0')).flat[0], + npf.ipmt(Decimal('0.1') / Decimal('12'), Decimal('1'), + Decimal('24'), Decimal('2000'), + Decimal('0'), 'end').flat[0]) + + def test_broadcast(self): + assert_almost_equal(npf.nper(0.075, -2000, 0, 100000., [0, 1]), + [21.5449442, 20.76156441], 4) + + assert_almost_equal(npf.ipmt(0.1 / 12, list(range(5)), 24, 2000), + [-17.29165168, -16.66666667, -16.03647345, + -15.40102862, -14.76028842], 4) + + assert_almost_equal(npf.ppmt(0.1 / 12, list(range(5)), 24, 2000), + [-74.998201, -75.62318601, -76.25337923, + -76.88882405, -77.52956425], 4) + + assert_almost_equal(npf.ppmt(0.1 / 12, list(range(5)), 24, 2000, 0, + [0, 0, 1, 'end', 'begin']), + [-74.998201, -75.62318601, -75.62318601, + -76.88882405, -76.88882405], 4) + + def test_broadcast_decimal(self): + # Use almost equal because precision is tested in the explicit tests, + # this test is to ensure broadcast with Decimal is not broken. + assert_almost_equal(npf.ipmt(Decimal('0.1') / Decimal('12'), + list(range(5)), Decimal('24'), + Decimal('2000')), + [Decimal('-17.29165168'), Decimal('-16.66666667'), + Decimal('-16.03647345'), Decimal('-15.40102862'), + Decimal('-14.76028842')], 4) + + assert_almost_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), + list(range(5)), Decimal('24'), + Decimal('2000')), + [Decimal('-74.998201'), Decimal('-75.62318601'), + Decimal('-76.25337923'), Decimal('-76.88882405'), + Decimal('-77.52956425')], 4) + + assert_almost_equal(npf.ppmt(Decimal('0.1') / Decimal('12'), + list(range(5)), Decimal('24'), + Decimal('2000'), Decimal('0'), + [Decimal('0'), Decimal('0'), Decimal('1'), + 'end', 'begin']), + [Decimal('-74.998201'), Decimal('-75.62318601'), + Decimal('-75.62318601'), Decimal('-76.88882405'), + Decimal('-76.88882405')], 4) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..741f505 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration + config = Configuration(None, parent_package, top_path) + config.add_subpackage('numpy_financial') + return config + + +if __name__ == "__main__": + from numpy.distutils.core import setup + + setup(name='numpy-financial', + version='0.0.1', + configuration=configuration)