Search code examples
pythonoptimizationmontecarloquantitative-financeportfolio

Using Monte Carlo for portfolio optimisation but must satisfy constraints


I have a problem trying to find portfolio weights and optimise them while satisfying constraints. I require some help on designing a code that would allow me to simulate multiple arrays of weights while satisfying a range of constraints, please see example below for an explanation:

Problem - simulate various weights while satisfying constraints:

Instruments = ['Fixed Income', 'Equity 1', 'Equity 2', 'Multi-Asset', 'Cash']

Constraints:

  1. each weight between 0.1 and 0.4
  2. cash = 0.05
  3. Equity 1 less than 0.3

At the moment I have code:

import numpy as np:

instruments = ['Fixed Income', 'Equity 1', 'Equity 2', 'Multi-Asset', 'Cash']

weights = np.random.uniform(0.1, 0.4, len(instruments)) # random number generation
weights = weights / weights.sum() # normalise weights
# I have done test runs, and normalised weights always fit the constraints

if weights[-1] > 0.03:
   excess = weights[-1] - 0.03
   # distribute excess weights equally
   weights[:-1] = weights[:-1] + excess / (len(weights) - 1)

and I'm stuck, I also realised that when I distribute the excess weights I have effectively broken my constraint.

Is there anyway to do it? and I have to do it via monte-carlo

Thanks everyone for your help.


Solution

  • Here is one solution:

    import numpy as np
    N = 1000000
    instruments = ['Fixed Income', 'Equity 1', 'Equity 2', 'Multi-Asset', 'Cash']
    
    # each row is set of weights in order of instruments above
    weights = np.zeros(shape=(N, len(instruments)))
    
    weights[:, -1] = 0.05
    weights[:, 1] = np.random.uniform(0, 0.3, N)
    
    cols = (0, 2, 3)
    
    # fill columns with random numbers
    for col in cols[0:-1]:
        w_remaining = 1 - np.sum(weights, axis=1)
        weights[:, col] = np.random.uniform(0.1, 0.4, N)
    
    # the last column is constrained for normalization
    weights[:, cols[-1]] = 1 - np.sum(weights, axis=1)
    
    # select only rows that meet condition:
    cond1 = np.all(0.1 <= weights[:, cols], axis=1)
    cond2 = np.all(weights[:, cols] <= 0.4, axis=1)
    valid_rows = cond1*cond2
    
    weights = weights[valid_rows, :]
    
    # verify sum of weights == 1:
    np.testing.assert_allclose(np.sum(weights, axis=1), 1)
    

    This solution is performant, but discards generated examples that don't satisfy the constraints.