简体   繁体   中英

Python: get every possible combination of weights for a portfolio

I think this problem can be solved using either itertools or cartesian, but I'm fairly new to Python and am struggling to use these:

I have a portfolio of 5 stocks, where each stock can have a weighting of -0.4, -0.2, 0, 0.2 or 0.4, with weightings adding up to 0. How do I create a function that produces a list of every possible combination of weights. eg [-0.4, 0.2, 0, 0.2, 0]... etc

Ideally, the function would work for n stocks, as I will eventually want to do the same process for 50 stocks.

edit: To clarify, I'm looking for all combinations of length n (in this case 5), summing to 0. The values can repeat : eg: [0.2, 0.2, -0.4, 0, 0], [ 0.4, 0, -0.2, -0.2, 0.4], [0,0,0,0.2,-0.2], [0, 0.4, -0.4, 0.2, -0.2] etc. So [0,0,0,0,0] would be a possible combination. The fact that there are 5 possible weightings and 5 stocks is a coincidence (which i should have avoided!), this same question could be with 5 possible weightings and 3 stocks or 7 stocks. Thanks.

Something like this, although it's not really efficient.

from decimal import Decimal
import itertools

# possible optimization: use integers rather than Decimal
weights = [Decimal("-0.4"), Decimal("-0.2"), Decimal(0), Decimal("0.2"), Decimal("0.4")]

def possible_weightings(n = 5, target = 0):
    for all_bar_one in itertools.product(weights, repeat = n - 1):
        final = target - sum(all_bar_one)
        if final in weights:
            yield all_bar_one + (final,)

I repeat from comments, you cannot do this for n = 50 . The code yields the right values, but there isn't time in the universe to iterate over all the possible weightings.

This code isn't brilliant. It does some unnecessary work examining cases where, for example, the sum of all but the first two is already greater than 0.8 and so there's no point separately checking all the possibilities for the first of those two.

So, this does n = 5 in nearly no time, but there is some value of n where this code becomes infeasibly slow, and you could get further with better code. You still won't get to 50. I'm too lazy to write that better code, but basically instead of all_bar_one you can make recursive calls to possible_weightings with successively smaller values of n and a value of target equal to the target you were given, minus the sum you have so far. Then prune all the branches you don't need to take, by bailing out early in cases where target is too large (positive or negative) to be reached using only n values.

I understand the values can repeat, but all have to sum to zero, therefore the solution might be:

>>> from itertools import permutations
>>> weights = [-0.4, -0.2, 0, 0.2, 0.4]
>>> result = (com for com in permutations(weights) if sum(com)==0)
>>> for i in result: print(i)

edit: you might use product as @Steve Jassop suggested.

combi = (i for i in itertools.product(weights, repeat= len(weights)) if not sum(i))
for c in combi:
    print(c)

I like using the filter function:

from itertools import permutations
w = [-0.4, -0.2, 0, 0.2, 0.4] 

def foo(w):
    perms = list(permutations(w))
    sum0 = filter(lambda x: sum(x)==0, perms)
    return sum0

print foo(w)

Different approach.

1 Figure out all sequences of the weights that add up to zero, in order.

for example, these are some possibilities (using whole numbers to type less):
[0, 0, 0, 0, 0]
[-4, 0, 0, +2, +2]
[-4, 0, 0, 0, +4]

[-4, +4, 0, 0, 0] is incorrect because weights are not picked in order.

2 Permute what you got above, because the permutations will add up to zero as well.

This is where you'd get your [-4, 0, 0, 0, +4] and [-4, +4, 0, 0, 0]

OK, being lazy. I am going to pseudo-code/comment-code a good deal of my solution. Not that strong at recursion, the stuff is too tricky to code quickly and I have doubts that this type of solution scales up to 50.

ie I don't think I am right, but it might give someone else an idea.

def find_solution(weights, length, last_pick, target_sum):

    # returns a list of solutions, in growing order, of weights adding up to the target_sum

    # weights are the sequence of possible weights - IN ORDER, NO REPEATS
    # length is how many weights we are adding up
    # last_pick - the weight picked by the caller 
    # target_sum is what we are aiming for, which will always be >=0

    solutions = []

    if length > 1:

        #since we are picking in order, having picked 0 "disqualifies" -4 and -2.
        if last_pick > weights[0]:
            weights = [w for w in weights if w >= last_pick]

        #all remaining weights are possible
        for weight in weights:
            child_target_sum = target_sum + weight

            #basic idea, we are picking in growing order
            #if we start out picking +2 in a [-4,-2,0,+2,+4] list in order, then we are constrained to finding -2
            #with just 2 and 4 as choices.  won't work.
            if child_target_sum <= 0:
                break

            child_solutions = find_solution(weights, length=length-1, last_pick=weight, target_sum=child_target_sum)

            [solutions.append([weight] + child ) for child in child_solutions if child_solution]

    else:
        #only 1 item to pick left, so it has be the target_sum
        if target_sum in weights:
            return [[target_sum]]

    return solutions

weights = list(set(weights))
weights.sort()

#those are not permutated yet
solutions = find_solutions(weights, len(solution), -999999999, 0)

permutated = []
for solution in solutions:
   permutated.extend(itertools.permutations(solution))

If you just want a list of all the combinations, use itertools.combinations :

w = [-0.4, -0.2, 0, 0.2, 0.4]
l = len(w)

if __name__ == '__main__':
    for i in xrange(1, l+1):
        for p in itertools.combinations(w, i):
            print p

If you want to count the different weights that can be created with these combinations, it's a bit more complicated.

First, you generate permutations with 1, 2, 3, ... elements. Then you take the sum of them. Then you add the sum to the set (will no do anything if the number is already present, very fast operation). Finally you convert to a list and sort it.

from itertools import combinations

def round_it(n, p):
    """rounds n, to have maximum p figures to the right of the comma"""
    return int((10**p)*n)/float(10**p)

w = [-0.4, -0.2, 0, 0.2, 0.4]
l = len(w)
res = set()

if __name__ == '__main__':
    for i in xrange(1, l+1):
        for p in combinations(w, i):
            res.add(round_it(sum(p), 10))  # rounding necessary to avoid artifacts

    print sorted(list(res))

Is this what you are looking for: if L = [-0.4, 0.2, 0, 0.2, 0]

AllCombi = itertools.permutations(L)

for each in AllCombi:
    print each

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM