问题
I have a DataFrame
as follows :
Name Volatility Return
a 0.0243 0.212
b 0.0321 0.431
c 0.0323 0.443
d 0.0391 0.2123
e 0.0433 0.3123
I'd like to have a Volatility
of 0.035
and the maximized Return
for that volatility.
That is, I'd like, in a new Df
the Name
and the percentage of that asset that will be in my portfolio
that gives the maximum Return
for a Volatility
equals to 0.035
.
Therefore, I need to solve a system of equations with multiple conditions, to obtain the best solution (HighestReturn
) for a fixed outcome (Volatility == 0.035
).
The conditions are:
- Each asset has a weight between 0 and 1.
- The sum of the weights is 1.
- The sum of the weights times the volatility of each asset is the "Desired Volatility".
- The sum of the weights times the return of each asset is the "Total Return". This should be maximized.
回答1:
Here is an approach using Z3Py, an open source SAT/SMT solver. In a SAT/SMT solver you can write your code just as a list of conditions, and the program finds an optimal solution (or just a solution that satisfies all the conditions when Z3 is used as solver).
Originally SAT solvers only worked with pure boolean expressions, but modern SAT/SMT solvers also allow for fixed-bit and unlimited integers, fractions, reals and even functions as central variable.
To write the given equations into Z3, they are converted quite literally into Z3 expressions. The code below comments each of the steps.
import pandas as pd
from z3 import *
DesiredVolatility = 0.035
df = pd.DataFrame(columns=['Name', 'Volatility', 'Return'],
data=[['a', 0.0243, 0.212],
['b', 0.0321, 0.431],
['c', 0.0323, 0.443],
['d', 0.0391, 0.2123],
['e', 0.0433, 0.3123]])
# create a Z3 instance to optimize something
s = Optimize()
# the weight of each asset, as a Z3 variable
W = [Real(row.Name) for row in df.itertuples()]
# the total volatility
TotVol = Real('TotVol')
# the total return, to be maximized
TotReturn = Real('TotReturn')
# weights between 0 and 1, and sum to 1
s.add(And([And(w >= 0, w <= 1) for w in W]))
s.add(Sum([w for w in W]) == 1)
# the total return is calculated as the weighted sum of the asset returns
s.add(TotReturn == Sum([w * row.Return for w, row in zip(W, df.itertuples())]))
# the volatility is calculated as the weighted sum of the asset volatility
s.add(TotVol == Sum([w * row.Volatility for w, row in zip(W, df.itertuples())]))
# the volatility should be equal to the desired volatility
s.add(TotVol == DesiredVolatility)
# we're maximizing the total return
h1 = s.maximize(TotReturn)
# we ask Z3 to do its magick
res = s.check()
# we check the result, hoping for 'sat': all conditions satisfied, a maximum is found
if res == sat:
s.upper(h1)
m = s.model()
#for w in W:
# print(f'asset {w}): {m[w]} = {m[w].numerator_as_long() / m[w] .denominator_as_long() : .6f}')
# output the total return
print(f'Total Return: {m[TotReturn]} = {m[TotReturn].numerator_as_long() / m[TotReturn] .denominator_as_long() :.6f}')
# get the proportions out of the Z3 model
proportions = [m[w].numerator_as_long() / m[w] .denominator_as_long() for w in W]
# create a dataframe with the result
df_result = pd.DataFrame({'Name': df.Name, 'Proportion': proportions})
print(df_result)
else:
print("No satisfiable solution found")
Result:
Total Return: 452011/1100000 = 0.410919
Name Proportion
0 a 0.000000
1 b 0.000000
2 c 0.754545
3 d 0.000000
4 e 0.245455
You can easily add additional constraints, for example "no asset can have more than 30% of the total":
# change
s.add(And([And(w >= 0, w <= 1) for w in W]))`
# to
s.add(And([And(w >= 0, w <= 0.3) for w in W]))`
Which would result in:
Total Return: 558101/1480000 = 0.377095
Name Proportion
0 a 0.082432
1 b 0.300000
2 c 0.300000
3 d 0.017568
4 e 0.300000
来源:https://stackoverflow.com/questions/59790285/how-to-solve-a-system-of-equations-and-constraints-for-portfolio-optimization