0

I have a portfolio of 40 stocks and I'm trying to calculate the standard deviation for the total portfolio while changing the weight per stock every time I calculate this standard deviation. I know how to create a list of random numbers summing to 1, but how can I add max and min weights per stock..

The maximum I'm using is 4.5% and the minimum 0.5%.

The code I use for creating a list of random numbers summing to 1 is:

import numpy as np, numpy.random

random = np.random.dirichlet(np.ones(10), size=1)

But how can I make this list with only values between 0.005 and 0.045?

Dirk Koomen
  • 125
  • 2
  • 10
  • you want a random size in the range from 4.5% and 0.5%? – Matiiss Dec 29 '21 at 13:22
  • @Matiiss yes, I need a list of 40 values that are between 0.005 and 0.045 (0.5% and 4.5%) – Dirk Koomen Dec 29 '21 at 13:27
  • Does it have to be uniformly distributed? (It cannot be.) Or what are the requirements? – DanielTuzes Dec 29 '21 at 13:27
  • do you just need a list of values in that range `lst = [x / 1000 for x in range(5, 45 + 1)]`? or do you need a list of `np.radnom.dirichlet`s with sizes in that range `lst = [np.random.dirichlet(np.ones(10), size=x / 1000) for x in range(5, 45 + 1)]`? – Matiiss Dec 29 '21 at 13:30
  • @Matiiss I just need a list of values in that range with total sum equal to 1, ```np.random.dirichlet``` is not necessary. So i you know another way to fix it, let me know. – Dirk Koomen Dec 29 '21 at 13:31
  • @DanielTuzes what i need is a list of 40 numbers that are between 0.005 and 0.045 and have a total sum of 1. No particular distribution required, just need that list of numbers. Thanks! – Dirk Koomen Dec 29 '21 at 13:33
  • 1
    This is related: https://stackoverflow.com/questions/29187044/generate-n-random-numbers-within-a-range-with-a-constant-sum – dankal444 Dec 29 '21 at 13:36
  • Does this answer your question? [Is there an efficient way to generate N random integers in a range that have a given sum or average?](https://stackoverflow.com/questions/61393463/is-there-an-efficient-way-to-generate-n-random-integers-in-a-range-that-have-a-g) – Peter O. Dec 29 '21 at 17:43

2 Answers2

2

Consider using the following:

import numpy as np
first_half = np.random.random(size=20)*0.04+0.005
second_half = 0.05-first_half

This is the right range, uniformly distributed, all random, however, the 2nd half is (anti)correlated with the 1st half. You can merge the two halves:

tot40elems = np.concatenate((first_half,second_half))
DanielTuzes
  • 2,494
  • 24
  • 40
  • Thank you for your answer, I will use this for now! But best would be a list with uncorrelated numbers. – Dirk Koomen Dec 29 '21 at 13:44
  • You can vote for or [accept an answer](https://stackoverflow.com/help/someone-answers) if you liked it. You can also detail what kind of distribution or correlation is accepted. If you define the distribution and the number of independent random variables, the sum will be a mathematical fact, another distribution. – DanielTuzes Dec 29 '21 at 13:48
  • Do you know how to make the size flexible? Because sometimes I may want to use size 40 (like here), but sometimes I may want to use, for example, size 50. Do you know how to fix this? – Dirk Koomen Dec 29 '21 at 15:34
1

Perhaps using np.random.normal would give better results. You could scale down the distribution to the 0.005-0.045 range using the proportion of 80% that is variable (above 0.005). Because normal distributions can still have outliers, it will be necessary to "retry" the calculation if the values go out of bounds (but that shouldn't happen too frequently unless you give a large standard deviation):

import numpy as np

def randStock(count=40,minR=0.005,maxR=0.045,sd=3):
    iterations = 0
    while True:
        iterations += 1
        r = np.random.normal(1,sd,count) #normal distribution
        r -= min(r) # offset to zero
        r /= max(r) # scale to 0..1
        r = minR + r/sum(r)*(maxR-minR)/(maxR+minR) # scale to range
        if min(r)>=minR and max(r)<=maxR: return r, iterations

Output:

for _ in range(10):
    s,i = randStock()
    print(*map("{:6.4f}".format,(sum(s),min(s),max(s))),i,"iterations")

[sum]  [min]  [max]  [mean] 
1.0000 0.0050 0.0404 0.0250 1 iterations
1.0000 0.0050 0.0409 0.0250 2 iterations
1.0000 0.0050 0.0395 0.0250 1 iterations
1.0000 0.0050 0.0411 0.0250 4 iterations
1.0000 0.0050 0.0410 0.0250 2 iterations
1.0000 0.0050 0.0428 0.0250 1 iterations
1.0000 0.0050 0.0433 0.0250 1 iterations
1.0000 0.0050 0.0424 0.0250 1 iterations
1.0000 0.0050 0.0371 0.0250 1 iterations
1.0000 0.0050 0.0446 0.0250 1 iterations

Note that this could be improved to randomize the lower bound a bit more and you can chose a different standard deviations

Alain T.
  • 40,517
  • 4
  • 31
  • 51
  • Thank you! This seems to work for a portfolio of 40 stocks. But do you also know how to modify it so it can be used for any number of stocks between 22 and 200 (because of min and max)? For example, i tried randStock(count=80), and that gives me a sum of 1.2. I need it to be 1 to use it. – Dirk Koomen Jan 05 '22 at 14:15
  • For 80 stocks, the average will be 0.0125 so your minR and maxR should be adjusted accordingly (something like minR = 0.0025 and maxR=0.0225). If you want to automatically compute the range or minR,maxR to be +/- 80% of the mean, you can make `minR=0.2/count` and `maxR=1.8/count` instead of having them as function arguments. – Alain T. Jan 05 '22 at 21:29
  • You may also want to use the [empirical rule](https://www.investopedia.com/terms/e/empirical-rule.asp) that states that 68% of the data should fall within 1 standard deviation, 95% within 2 standard deviations and 99.7% within 3 sd. Reverse engineering the sd parameter based on the desired range should produce a result in fewer iterations. – Alain T. Jan 05 '22 at 21:37