-1

I have a series of accounts which charge an interest rate and every day I must decide from which to borrow money. I have a borrowing limit for every account. Obviously the easiest answer is to exhaust the cheapest rates first and so on.

As we have to compute this several times a day, the number of accounts varies and we plan a roll-forward cash flow I'm trying to write some Python code in order to automate it.

The objective function to minimize is the weighted average interest rate, identifying the money to borrow from each account (x), bounded from 0 to an integer (depending on each agreement) in order to cover the outstanding.

The code below seems to be working fine, but I did the math on a piece of paper and it isn't reaching the global minimum. Is there anything I'm missing?

import numpy as np
from scipy.optimize import minimize

outstanding = -106332403


limit = [15000000, 29250000, 15000000, 22000000, 52567324, 5000000, 5000000, 40000000, 7398262]
interest = [0.73, 0.63, 0.78, 0.75, 0.6084, 0.97, 0.84, 0.625, 0.40]

limit = np.asarray(limit)
interest = np.asarray(interest)

def objective(x):
    product = x * interest
    sumproduct = sum(product)
    return sumproduct / -outstanding

def constraint1(x):
    return sum(x) + outstanding

# initial guesses
n = len(interest)
x0 = [1,1,1,1,1,1,1,1,1]

bnds = []

for value in limit:
    b= (0,value)
    bnds.append(b)


con1 = {'type': 'eq', 'fun': constraint1}
cons = (con1)

solution = minimize(objective,x0,method='SLSQP',bounds=bnds,constraints=cons)

x = solution.x
David Buck
  • 3,752
  • 35
  • 31
  • 35
  • What is your expected result (so others don't have to bother with differentiating your functions)? Your problem seems to be linear. Do you really need SLSQP to solve it? For linear problems might not be the best algorithm. – onodip Nov 26 '19 at 21:13
  • Thanks @onodip. No, I don't really need SLSQP. I just tried it because it accepts bounds and constraints. You don't need to differentiate. The expected result suggests to use the lower interest agreements first: 7.3 M at 40% / 52 M at 60.84% / 40 M at 62.5% and the remainder approx 7 M at 63%. The rest should not be used, so they'll be 0. – Diego Kenny Nov 27 '19 at 20:33
  • So the array result (in millions) should be: (0, 7, 0, 0, 52.6, 0, 0, 40, 7.3) – Diego Kenny Nov 27 '19 at 20:40
  • With this solution your equality constraint is not satisfied. I get the value 567597.0 for it, and it supposed to be zero. In this simple case it does not make trouble, but your objective and constraint are also a very different order of magnitude, it is good practice to normalize it. For me the optimization results in x=[14844753.39667935, 14844753.39667935, 14844753.39667935, 14844753.39667935, 14844753.39667935, 4955206.87349367, 4955206.87349367, 14844753.39667935, 7353468.87293655] and objective=0.6876659050587411 – onodip Nov 27 '19 at 20:52
  • Yes. That's the solution that results. But it is not correct and I don't know why. The solution I told (0, 7, 0, 0, 52.6, 0, 0, 40, 7.3) was approx numbers, that's why you don't get to 0 in the constraint. The thing is that scipy is not arriving at the minimum value and I don't know why. – Diego Kenny Nov 28 '19 at 19:36

1 Answers1

1

I made a couple of changes in your script. The most important was to set a larger maximum iteration, and playing a bit with the finite difference step and the convergence tolerance. I also rewrote your objective, to change the order of magnitude. This way I get a solution close to the values that you provided on your comment.

import numpy as np
from scipy.optimize import minimize

outstanding = -106332403


limit = np.array([15000000, 29250000, 15000000, 22000000, 52567324, 5000000, 5000000, 40000000, 7398262])
interest = np.array([0.73, 0.63, 0.78, 0.75, 0.6084, 0.97, 0.84, 0.625, 0.40])

# initial guesses
n = len(interest)
x0 = np.ones(len(interest)) * 1e6 * 1

def objective(x):
    return np.dot(x, interest)

def constraint1(x):
    con = sum(x) + outstanding
    return con

bnds = [(0, value) for value in limit]

con1 = {'type': 'eq', 'fun': constraint1}

solution = minimize(objective,x0,method='SLSQP',bounds=bnds,constraints=con1, 
                    options={"eps": 01e-3, "maxiter": 1000}, tol=1e-8)

print(solution)

Results in:

     fun: 63952359.431600004
     jac: array([0.72999299, 0.62999874, 0.77999383, 0.74999779, 0.60839951,
       0.96999854, 0.84000081, 0.6249994 , 0.40000677])
 message: 'Optimization terminated successfully.'
    nfev: 1635
     nit: 147
    njev: 145
  status: 0
 success: True
       x: array([0.00000000e+00, 6.36681700e+06, 0.00000000e+00, 0.00000000e+00,
       5.25673240e+07, 1.77575799e-16, 2.32417370e-17, 4.00000000e+07,
       7.39826200e+06])
onodip
  • 635
  • 7
  • 12