保持动态不等式约束在Python投资组合优化问题中的可行性



为了避免在投资组合优化问题中集中在顶部,我使用cVaR风格的不等式约束。例如,重量最小的25%的成分的重量加起来需要大于10%。

这个约束定义如下:

n_smallest   = int(len(constituents)*0.25)
con1         = {"type": "ineq", "fun": lambda x: np.sum(sorted(x)[:n_smallest])-0.1}

为了保持约束的可行性,我也以以下方式实现了非线性约束,设置了" keep_viable "标记为True:

n_smallest = int(number_of_constituents*key)
constraint = NonlinearConstraint(fun=lambda x: sum(sorted(x)[:n_smallest_temp]), lb=min_weight, ub=1, keep_feasible=True)

我正在使用以下优化器:

global_opt   = scipy.optimize.minimize(objective_function=sharpe, x0=x0, bounds=bounds, constraints=[con1]}

当检查从优化器得到的解时,不等式约束不满足,并且最小权重的25%成分的权重相加并不总是等于10%。

当我在下面运行多次时,我倾向于得到截然不同的结果,我相信这是由于不等式约束在每个优化步骤中都没有被评估。有人遇到过类似的问题并解决了吗?

1。是否有一种方法可以使scipy最小化优化器在每一步检查所有约束?

2。是否有另一种方法可以使用条件不等式约束?

我也尝试过使用不同的优化器,即差分进化优化器,但这也没有帮助。我将非常感谢您的帮助。

请在下面找到一个最小可重复的例子:

import numpy as np
import pandas as pd
#import warnings
#warnings.filterwarnings("ignore")
from random import gauss, randint
from scipy.optimize import minimize, NonlinearConstraint
from scipy.sparse.construct import rand
class PortfolioOptimization(object):
def __init__(self, number_of_constituents, ineq_constraints):
self.ineq_constraints   = ineq_constraints
self.num_const          = number_of_constituents
self.returns_const      = pd.DataFrame([self.generate_random_returns() for i in range(self.num_const)]).T #20 constituents
self.returns_objective  = pd.DataFrame(self.generate_random_returns(), columns=["portfolio_to_estimate"])
def generate_inequality_constraints(self, inequality_constraints, number_of_constituents):
constraints         = []
for key in inequality_constraints.keys():
min_weight      = inequality_constraints[key]
n_smallest_temp = int(number_of_constituents*key)
temp_constraint = {"type": "ineq", "fun": lambda x: sum(sorted(x)[:n_smallest_temp])-min_weight}
constraints.append(temp_constraint)
return constraints
def generate_nonlinear_constraints(self, inequality_constraints, number_of_constituents):
constraints         = []
for key in inequality_constraints.keys():
min_weight      = inequality_constraints[key]
n_smallest_temp = int(number_of_constituents*key)
temp_constraint = NonlinearConstraint(fun=lambda x: sum(sorted(x)[:n_smallest_temp]), lb=min_weight, ub=1, keep_feasible=True)
constraints.append(temp_constraint)
return constraints
def check_inequality_constraints(self, weights_dict):
ineq_constraints    = self.ineq_constraints
weights_dict        = {k: v for k, v in sorted(weights_dict.items(), reverse=True, key=lambda item: item[1])}
for key in ineq_constraints.keys():
min_weight      = ineq_constraints[key]
n_smallest_temp = int(self.num_const*key)
weight_n_small  = sum(list(weights_dict.values())[-n_smallest_temp:])
if weight_n_small<min_weight:
print(f"Concentration constraint not fulfilled. Smallest {key*100}% have weight of {weight_n_small} (<{min_weight})")
else:
print(f"Smallest {key*100}% have weight of {weight_n_small} (>{min_weight})")
def generate_random_returns(self, mean=0.015, volatility=0.03, datapoints=200):
#rand_inte       = randint(1,10)/10
random_numbers  = [gauss(mean, volatility) for i in range(datapoints)]
return random_numbers
def correlation_portfolio(self, weights):
#weights_dict        = {key: value for key, value in zip(range(self.num_const), weights)}
#self.check_inequality_constraints(weights_dict)
portfolio_returns   = pd.concat([np.sum(self.returns_const*weights, axis=1), self.returns_objective], axis=1)
portfolio_returns.rename(columns={0: 'portfolio_returns'}, inplace=True)
correlations    = portfolio_returns.corr()
correlation     = correlations["portfolio_returns"].loc["portfolio_to_estimate"]
#print(correlation)
return -correlation
def portfolio_optimization(self, ineq_constraints, non_linear_constraints=False):
bounds              = self.num_const *[(0,1)] #20 constituents, min weight 0, max weight 1
x0                  = self.num_const *[1/self.num_const ]
if non_linear_constraints:
constraints     = self.generate_nonlinear_constraints(inequality_constraints=ineq_constraints, number_of_constituents=number_of_const)
else:
constraints     = self.generate_inequality_constraints(inequality_constraints=ineq_constraints, number_of_constituents=number_of_const)
constraints         = [{"type": "eq", "fun": lambda x: np.sum(x)-1}]+constraints #weights add up to 1
constraints         = constraints+[{"type": "ineq", "fun": lambda x: np.sum([el for el in x if el<0.05])-0.5}] #sum of all weights bigger than 0.05 is smaller than 0.5
optimum             = minimize(self.correlation_portfolio, x0=x0, bounds=bounds, options={"disp": False}, constraints=constraints).x
weights_dict        = {key: value for key, value in zip(range(self.num_const), optimum)}
self.check_inequality_constraints(weights_dict)
return weights_dict
ineq_constraints= {0.25: 0.1, 0.5: 0.25, 0.75: 0.5, 0.9: 0.3} #smallest 25% need to have at least 10% weight, 50% smallest need to have at least 25% weight and 75% smallest need to have at least 50% weight
number_of_const = 30
pf_optim        = PortfolioOptimization(number_of_constituents=number_of_const, ineq_constraints=ineq_constraints)
weights_dict    = pf_optim.portfolio_optimization(ineq_constraints=ineq_constraints)
weights_dict    = pf_optim.portfolio_optimization(ineq_constraints=ineq_constraints, non_linear_constraints=True)

在循环内通过lambda表达式定义约束时需要小心。让我们考虑generate_inequality_constraints:

中的循环
inequality_constraints = {0.25: 0.1, 0.5: 0.25, 0.75: 0.5, 0.9: 0.3}
number_of_constituents = 30
constraints = []
for key in inequality_constraints.keys():
min_weight      = inequality_constraints[key]
n_smallest_temp = int(number_of_constituents*key)
temp_constraint = {"type": "ineq", "fun": lambda x: sum(sorted(x)[:n_smallest_temp])-min_weight}
constraints.append(temp_constraint)

这里的问题是变量min_weightn_smallest_temp不是lambda的局部。这意味着它们是在调用lambda时定义的,而不是在定义它们时定义的。因此,每个lambda表达式在循环结束时对min_weightn_smallest_temp使用相同的值!这可以很容易地通过评估同一点x0:

的每个约束来验证。
In [3]: x0 = np.ones(30)/30
...: for con in constraints:
...:      print(con['fun'](x0))
...:
0.5999999999999999
0.5999999999999999
0.5999999999999999
0.5999999999999999

每个约束返回相同的值。长话短说,您需要捕获变量:

的值。
inequality_constraints = {0.25: 0.1, 0.5: 0.25, 0.75: 0.5, 0.9: 0.3}
number_of_constituents = 30
constraints = []
for key in inequality_constraints.keys():
m = inequality_constraints[key]
k = int(number_of_constituents*key)
temp_constraint = {"type": "ineq", "fun": lambda x, k=k, m=m: sum(sorted(x)[:k])-m}
constraints.append(temp_constraint)

现在约束可以按预期计算:

In [5]: for con in constraints:
...:      print(con['fun'](x0))
...:
0.1333333333333333
0.24999999999999994
0.23333333333333328
0.5999999999999999

对其他约束进行类似处理。

最新更新