pythonpython-3.xoptimizationfinancescipy-optimize

Optimization on Financial Data in python


I am trying to calculate the breakeven price for a given IRR and getting stuck in scope's root solver. A simple example:

n = 10. # no of periods
years = np.arange(n) + 1
initial_investment = [-1000]
quantity_sold = np.full(n, 20)
price = np.full(n, 20)
revenue = quantity_sold * price
expense = np.full(n, 50)

cash_flow = np.concatenate([initial_investment, revenue - expense])

With this we can calculate the IRR and verify that we get NPV of 0 using numpy_financial:

npf.irr(cash_flow), npf.npv(0.32975, cash_flow)
# (0.32975, 0.008)

What I want to do is the opposite: say, given an IRR of 40%, what is the cash_flow needed for break-even price (NPV = 0).

I am using scipy.optimize.root but getting lost as how to structure that. I can invert npf.npv to get IRR:

optimize.root(npf.npv, args=cash_flow, x0=0.5)

but cash_flow is an array, even allowing for the values of that array to be constant. How would I solve for cash_flow given an IRR?


Solution

  • It's certainly possible, but it's not very meaningful unless you further constrain the problem (by, for example, fixing the initial investment). Otherwise, the system is underdetermined:

    import numpy as np
    import scipy.optimize
    
    n = 10  # no of periods
    years = np.arange(1, 1 + n)
    initial_investment = -1000
    quantity_sold = np.full(shape=n, fill_value=20)
    price = np.full(shape=n, fill_value=20.)
    revenue = quantity_sold * price
    expense = np.full(shape=n, fill_value=50.)
    cash_flow = np.concatenate(((initial_investment,), revenue - expense))
    
    # Forward calculation of IRR, shown without use of `npf`
    def npv(rate: float) -> float:
        return (cash_flow / (1 + rate)**np.arange(cash_flow.size)).sum()
    result = scipy.optimize.root_scalar(f=npv, x0=0.5, bracket=(0, 1))
    assert result.converged
    irr = result.root
    assert np.isclose(0.32975, irr, atol=0, rtol=1e-5)
    np.isclose(0, npv(irr), rtol=0, atol=1e-12)
    print('irr =', irr)
    
    # Back-calculation of cash flow, again without `npf`
    def npv_from_series(cash_flow: np.ndarray) -> float:
        return (cash_flow / (1 + irr)**np.arange(cash_flow.size)).sum()
    result = scipy.optimize.least_squares(
        fun=npv_from_series,
        x0=np.concatenate(((-1,), np.ones(n))),
        bounds=scipy.optimize.Bounds(
            lb=np.concatenate(((-np.inf,), np.zeros(n))),
            ub=np.concatenate(((0,), np.full(shape=n, fill_value=np.inf))),
        ),
    )
    assert result.success
    cash_flow = result.x
    print(f'Cash flow for IRR of {irr:.1%}:')
    print(cash_flow)
    print(f'NPV error: {result.fun[0]:.2e}')
    
    irr = 0.3297531334357468
    Cash flow for IRR of 33.0%:
    [-3.32043506  0.99765753  1.06177051  1.11807742  1.17345293  1.23855932
      1.33771217  1.5628038   3.13235102  0.0914474   0.674785  ]
    NPV error: 1.80e-16
    

    If you do fix the initial investment, it's still underdetermined, but at least tends to have a sensible scale:

    # Back-calculation of cash flow, again without `npf`
    def npv_from_series(profit: np.ndarray) -> float:
        cash_flow = np.concatenate(((initial_investment,), profit))
        return (cash_flow / (1 + irr)**np.arange(cash_flow.size)).sum()
    result = scipy.optimize.least_squares(
        fun=npv_from_series,
        x0=np.ones(n),
        bounds=scipy.optimize.Bounds(lb=np.zeros(n)),
    )
    assert result.success
    profit = result.x
    print(f'Profit series for IRR of {irr:.1%} and investment of ${-initial_investment:.2f}:')
    print(profit)
    print(f'NPV error: {result.fun[0]:.2e}')
    
    irr = 0.3297531334357468
    Profit series for IRR of 33.0% and investment of $1000.00:
    [577.58634499 435.70855853 328.53386135 247.66502124 186.69665115
     140.76042784 106.16648717  80.12334345  60.5226813   45.77384425]
    NPV error: -2.49e-14
    

    If you aggressively constrain the problem by assuming constant profit, the profit can be found analytically:

    constant_profit = -initial_investment/(
        (1 + irr)**np.arange(-1, -1-n, -1)
    ).sum()
    print(f'For IRR of {irr:.1%} and investment of ${-initial_investment:.2f}, '
          f'assumed-constant profit of ${constant_profit:.2f} over {n} years')
    
    For IRR of 33.0% and investment of $1000.00, assumed-constant profit of $350.00 over 10 years
    

    Yet another constrained solution is to fix the NPV terms to a constant value. This makes the profit a geometric series whose compound growth equals the IRR:

    profit = -initial_investment/n*(1 + irr)**np.arange(1, 1+n)
    print(f'For IRR of {irr:.1%} and investment of ${-initial_investment:.2f}, '
          f'constant-term profit over {n} years is')
    cash_flow = np.concatenate(((initial_investment,), profit))
    print(profit)
    print(f'NPV error: {npv(irr):.2e}')
    
    For IRR of 33.0% and investment of $1000.00, constant-term profit over 10 years is
    [ 132.97531334  176.82433959  235.13271964  312.66847071  415.77187865
      552.87395843  735.18587862  977.61572575 1299.98757461 1728.66255077]
    NPV error: 0.00e+00