0

I've been trying to add constraints for my random non integer number generation. I am working on a portfolio optimization process and looking to constraint weights of random generetad portfolios.

I managed to put constraints somehow but I could not limit the sum of weights to 100% while keeping each weight between its own threshold.

You may realize that I removed the following statement because it leads to weights above or below the limits given before.

#weights = weights/np.sum(weights)

#Create Random Weighted Portfolios for 5 Assets

num_assets = 5
num_portfolios = 1000
p_weights = []

for portfolio in range(num_portfolios):

    w1 = random.uniform(0.5, 0.7)
    w2 = random.uniform(0.05, 0.3)
    w3 = random.uniform(0.05, 0.3)
    w4 = random.uniform(0.05, 0.5)
    w5 = random.uniform(0.03, 0.15)

    k = w1 + w2 + w3 + w4 + w5 

    #Sum of Asset weights is 100%


    weights = (w1, w2, w3, w4, w5)


    #weights = weights/np.sum(weights)
    p_weights.append(weights)



w_data_matrix =  np.asmatrix(p_weights)
print(w_data_matrix)


skatip
  • 15
  • 6
  • What was the problem with weights/np.sum(weights) ? You're telling you want to achieve but you don't tell about the problm – azro Apr 25 '20 at 21:27
  • Your problem appears to be similar to this question: https://stackoverflow.com/questions/61393463/is-there-an-efficient-way-to-generate-a-combination-of-random-integers-in-a-rang – Peter O. Apr 25 '20 at 21:33
  • You have to normalize your weights to add up to 100% – plr Apr 25 '20 at 21:43
  • Unfortunately normalizing is not the situation here since after that initial criteria gets breached. I am looking for a way to generate random numbers providing the condition in advance. – skatip Apr 26 '20 at 11:48

2 Answers2

2

What about this:

for portfolio in range(num_portfolios):
    weights = [ random.uniform(0.5, 0.7),
                random.uniform(0.05, 0.3),
                random.uniform(0.05, 0.3),
                random.uniform(0.05, 0.5),
                random.uniform(0.03, 0.15) ]
    k = sum(weights)
    weights = [ w/k for w in weights ]
    p_weights.append(weights)
Błotosmętek
  • 12,717
  • 19
  • 29
  • Thank you but I think this results in same way with adding #weights = weights/np.sum(weights) When weights are normalized with a loop like this afterwards, first criteria set gets breached. For example w1 receives less than 50% – skatip Apr 26 '20 at 11:43
  • Then your criteria set is flawed. – Błotosmętek Apr 26 '20 at 11:54
  • Unlikely, it is achievable to match all these criteria. When weights are generated without a criteria of sum of them equals 100%, total sum exceeds 100% and if you divide each weight by the sum of all it reduces the intially generated number. In some cases below criteria threshold. There should be a way to generate weights summing to 100% and keeping them in their thresholds. – skatip Apr 26 '20 at 14:29
  • Basic math says NOPE. – Błotosmętek Apr 26 '20 at 14:35
  • I thought so as well but check this: w1 0.62312553 w2 0.07535596 w3 0.19843803 w4 0.32300224 w5 0.04776013 sums to 1.2676 and if you divide 0.62312553 by 1.2676 you ll get 0.4915 – skatip Apr 26 '20 at 14:55
  • By "nope" I meant that if each of your variables has to be in a specified range yet their sum needs to be in range (0,1) then they are no longer uniform. – Błotosmętek Apr 26 '20 at 15:15
  • Yes, I got it. This is pre conditioned so the distribution will not be uniform but I do not know how to generate random weights like this since they are non integer. – skatip Apr 26 '20 at 15:19
0

Here is a Python solution to your question. This one has the following advantages:

  • It does not use rejection sampling.
  • It chooses uniformly at random from among all combinations that meet the requirements.

It's a modification I made based on an algorithm by John McClane, which he posted as an answer to another question. I describe the algorithm in another answer.

import random # Or secrets

def _getSolTableForRanges(ranges, adjsum):
        n = len(ranges)
        t = [[0 for i in range(adjsum + 1)] for j in range(n + 1)]
        t[0][0] = 1
        for i in range(1, n + 1):
            for j in range(0, adjsum + 1):
                krange = ranges[i-1][1] - ranges[i-1][0]
                jm = max(j - krange, 0)
                v = 0
                for k in range(jm, j + 1):
                    v += t[i - 1][k]
                t[i][j] = v
        return t

def intsInRangesWithSum(numSamples, ranges, total):
        """ Generates one or more combinations of
           'len(ranges)' numbers each, where each
           combination's numbers sum to 'total', and each number
           has its own valid range.  'ranges' is a list of valid ranges
           for each number; the first item in each range is the minimum
           value and the second is the maximum value.  For example,
           'ranges' can be [[1,4],[3,5],[2,6]], which says that the first
           number must be in the interval [1, 4], the second in [3, 5],
           and the third in [2, 6].
            The combinations are chosen uniformly at random.
               Neither the integers in the 'ranges' list nor
           'total' may be negative.  Returns an empty
           list if 'numSamples' is zero.
            This is a modification I made to an algorithm that
              was contributed in a _Stack Overflow_
          answer (`questions/61393463`) by John McClane.
          Raises an error if there is no solution for the given
          parameters.  """
        mintotal = sum([x[0] for x in ranges])
        maxtotal = sum([x[1] for x in ranges])
        adjsum = total - mintotal
        print([total,adjsum])
        # Min, max, sum negative
        if total<0: raise ValueError
        for r in ranges:
          if r[0]<0 or r[1]<0: raise ValueError
        # No solution
        if mintotal > total or maxtotal < total:
            raise ValueError
        if numSamples == 0:
            return []
        # One solution
        if maxtotal == total:
            return [[x[1] for x in ranges] for i in range(numSamples)]
        if mintotal == total:
            return [[x[0] for x in ranges] for i in range(numSamples)]
        samples = [None for i in range(numSamples)]
        numPerSample = len(ranges)
        table = _getSolTableForRanges(ranges, adjsum)
        for sample in range(numSamples):
            s = adjsum
            ret = [0 for i in range(numPerSample)]
            for ib in range(numPerSample):
                i = numPerSample - 1 - ib
                # Or secrets.randbelow(table[i + 1][s])
                v = random.randint(0, table[i + 1][s] - 1)
                r = ranges[i][0]
                v -= table[i][s]
                while v >= 0:
                    s -= 1
                    r += 1
                    v -= table[i][s]
                ret[i] = r
            samples[sample] = ret
        return samples

Example:

weights=intsInRangesWithSum(
   # One sample
   1,
   # Ranges for each random number
   [[50, 70], [5, 30], [5, 30], [5, 50], [3, 15]],
   # Sum of the numbers
   100)
# Divide by 100 to get weights that sum to 1
weights=[x/100.0 for x in weights[0]]
Peter O.
  • 32,158
  • 14
  • 82
  • 96