# 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).
import numpy as np

__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):
    try:
        return _when_to_num[when]
    except KeyError:
        return [_when_to_num[x] for x in when]


def fv(rate, nper, pmt, pv, when='end'):
    """
    Compute the future value.

    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

    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])

    """
    when = _convert_when(when)
    rate, nper, pmt, pv, when = map(np.asarray, [rate, nper, pmt, pv, when])
    temp = (1+rate)**nper
    miter = np.broadcast(rate, nper, pmt, pv, when)
    zer = np.zeros(miter.shape)
    fact = np.where(rate==zer, nper+zer, (1+rate*when)*(temp-1)/rate+zer)
    return -(pv*temp + pmt*fact)

def pmt(rate, nper, pv, fv=0, when='end'):
    """
    Compute the payment against loan principal plus interest.

    Parameters
    ----------
    rate : array_like
        Rate of interest (per period)
    nper : array_like
        Number of compounding periods
    pv : array_like
        Present value
    fv : array_like
        Future value
    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 ``pmt`` 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

    Examples
    --------
    What would the monthly payment need to be 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.

    """
    when = _convert_when(when)
    rate, nper, pv, fv, when = map(np.asarray, [rate, nper, pv, fv, when])
    temp = (1+rate)**nper
    miter = np.broadcast(rate, nper, pv, fv, when)
    zer = np.zeros(miter.shape)
    fact = np.where(rate==zer, nper+zer, (1+rate*when)*(temp-1)/rate+zer)
    return -(fv + pv*temp) / fact

def nper(rate, pmt, pv, fv=0, when='end'):
    """
    Compute the number of periods.

    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

    or, when ``rate == 0``::

      fv + pv + pmt * nper == 0

    Examples
    --------
    If you only had $150 to spend as payment, how long would it take to pay-off
    a loan of $8,000 at 7% annual interest?

    >>> np.nper(0.07/12, -150, 8000)
    64.073348770661852

    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.06/12:0.071/12:0.01/12, -200:-99:100, 6000:7001:1000]))
    array([[[ 32.58497782,  38.57048452],
            [ 71.51317802,  86.37179563]],
           [[ 33.07413144,  39.26244268],
            [ 74.06368256,  90.22989997]]])

    """
    when = _convert_when(when)
    rate, pmt, pv, fv, when = map(np.asarray, [rate, pmt, pv, fv, when])
    try:
        z = pmt*(1.0+rate*when)/rate
    except ZeroDivisionError:
        z = 0.0
    A = -(fv + pv)/(pmt+0.0)
    B = np.log((-fv+z) / (pv+z))/np.log(1.0+rate)
    miter = np.broadcast(rate, pmt, pv, fv, when)
    zer = np.zeros(miter.shape)
    return np.where(rate==zer, A+zer, B+zer) + 0.0

def ipmt(rate, per, nper, pv, fv=0.0, when='end'):
    """
    Not implemented. Compute the payment portion for loan interest.

    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``

    """
    total = pmt(rate, nper, pv, fv, when)
    # Now, compute the nth step in the amortization
    raise NotImplementedError

def ppmt(rate, per, nper, pv, fv=0.0, when='end'):
    """
    Not implemented. 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(rate, nper, pmt, fv=0.0, when='end'):
    """
    Compute the present value.

    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 ``pv`` 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

    """
    when = _convert_when(when)
    rate, nper, pmt, fv, when = map(np.asarray, [rate, nper, pmt, fv, when])
    temp = (1+rate)**nper
    miter = np.broadcast(rate, nper, pmt, fv, when)
    zer = np.zeros(miter.shape)
    fact = np.where(rate == zer, nper+zer, (1+rate*when)*(temp-1)/rate+zer)
    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)

# 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.
def rate(nper, pmt, pv, fv, when='end', guess=0.10, tol=1e-6, 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 : float, optional
        Starting guess for solving the rate of interest
    tol : float, optional
        Required tolerance for the solution
    maxiter : int, optional
        Maximum iterations in finding the solution

    Notes
    -----
    The rate of interest ``rate`` is computed by solving the equation::

     fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate * ((1+rate)**nper - 1) = 0

    or, if ``rate = 0``::

     fv + pv + pmt * nper = 0

    """
    when = _convert_when(when)
    nper, pmt, pv, fv, when = map(np.asarray, [nper, pmt, pv, fv, when])
    rn = guess
    iter = 0
    close = False
    while (iter < maxiter) and not close:
        rnp1 = rn - _g_div_gp(rn, nper, pmt, pv, fv, when)
        diff = abs(rnp1-rn)
        close = np.all(diff<tol)
        iter += 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(values):
    """
    Return the Internal Rate of Return (IRR).

    This is the rate of return that gives a net present value of 0.0.

    Parameters
    ----------
    values : array_like, shape(N,)
        Input cash flows per time period.  At least the first value would be
        negative to represent the investment in the project.

    Returns
    -------
    out : float
        Internal Rate of Return for periodic input values.

    Examples
    --------
    >>> np.irr([-100, 39, 59, 55, 20])
    0.2809484211599611

    """
    res = np.roots(values[::-1])
    # Find the root(s) between 0 and 1
    mask = (res.imag == 0) & (res.real > 0) & (res.real <= 1)
    res = res[mask].real
    if res.size == 0:
        return np.nan
    rate = 1.0/res - 1
    if rate.size == 1:
        rate = rate.item()
    return rate

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.  Must be the same
        increment as the `rate`.

    Returns
    -------
    out : float
        The NPV of the input cash flow series `values` at the discount `rate`.

    Notes
    -----
    Returns the result of:

    .. math :: \\sum_{t=1}^M{\\frac{values_t}{(1+rate)^{t}}}

    """
    values = np.asarray(values)
    return (values / (1+rate)**np.arange(1,len(values)+1)).sum(axis=0)

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.
    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)
    pos = values > 0
    neg = values < 0
    if not (pos.size > 0 and neg.size > 0):
        return np.nan

    n = pos.size + neg.size
    numer = -npv(reinvest_rate, values[pos])*((1+reinvest_rate)**n)
    denom = npv(finance_rate, values[neg])*(1+finance_rate)
    return (numer / denom)**(1.0/(n-1)) - 1
