如何解决投资组合优化的方程组和约束条件?
How to solve a system of equations and constraints for portfolio optimization?
我有一个 DataFrame
如下:
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
我想要 0.035
的 Volatility
和该波动率的最大化 Return
。
也就是说,我想在新的 Df
中 Name
以及将在我的 portfolio
中提供最大 [=14] 的资产百分比=] 对于 Volatility
等于 0.035
.
因此,我需要求解具有多个条件的方程组,以获得固定结果(Volatility == 0.035
)的最佳解(最高Return
)。
条件是:
- 每项资产的权重都在 0 到 1 之间。
- 权重之和为1
- 每种资产的权重乘以波动率的总和是"Desired Volatility"。
- 每个资产的权重乘以 return 的总和就是 "Total Return"。这应该最大化。
这是一种使用 Z3Py, an open source SAT/SMT 求解器的方法。
在 SAT/SMT 求解器中,您可以将代码编写为条件列表,程序会找到一个最佳解决方案(或者当 Z3 用作求解器时,只是满足所有条件的解决方案)。
最初 SAT 求解器仅使用纯布尔表达式,但现代 SAT/SMT 求解器还允许固定位和无限整数、分数、实数甚至函数作为中心变量。
为了将给定的方程式写入 Z3,它们会按字面意思转换为 Z3 表达式。下面的代码注释了每个步骤。
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")
结果:
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
您可以轻松添加其他约束,例如 "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]))`
这将导致:
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
我有一个 DataFrame
如下:
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
我想要 0.035
的 Volatility
和该波动率的最大化 Return
。
也就是说,我想在新的 Df
中 Name
以及将在我的 portfolio
中提供最大 [=14] 的资产百分比=] 对于 Volatility
等于 0.035
.
因此,我需要求解具有多个条件的方程组,以获得固定结果(Volatility == 0.035
)的最佳解(最高Return
)。
条件是:
- 每项资产的权重都在 0 到 1 之间。
- 权重之和为1
- 每种资产的权重乘以波动率的总和是"Desired Volatility"。
- 每个资产的权重乘以 return 的总和就是 "Total Return"。这应该最大化。
这是一种使用 Z3Py, an open source SAT/SMT 求解器的方法。 在 SAT/SMT 求解器中,您可以将代码编写为条件列表,程序会找到一个最佳解决方案(或者当 Z3 用作求解器时,只是满足所有条件的解决方案)。
最初 SAT 求解器仅使用纯布尔表达式,但现代 SAT/SMT 求解器还允许固定位和无限整数、分数、实数甚至函数作为中心变量。
为了将给定的方程式写入 Z3,它们会按字面意思转换为 Z3 表达式。下面的代码注释了每个步骤。
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")
结果:
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
您可以轻松添加其他约束,例如 "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]))`
这将导致:
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