mirror of
https://github.com/robertmartin8/PyPortfolioOpt.git
synced 2022-11-27 18:02:41 +03:00
Merge branch 'v1.5.0' of https://github.com/robertmartin8/PyPortfolioOpt into v1.5.0
This commit is contained in:
@@ -9,10 +9,14 @@ evaluate return and risk for a given set of portfolio weights.
|
||||
import collections
|
||||
import json
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import cvxpy as cp
|
||||
import scipy.optimize as sco
|
||||
|
||||
from . import objective_functions
|
||||
from . import exceptions
|
||||
|
||||
@@ -174,12 +178,11 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
self._constraints = []
|
||||
self._lower_bounds = None
|
||||
self._upper_bounds = None
|
||||
self._map_bounds_to_constraints(weight_bounds)
|
||||
|
||||
self._opt = None
|
||||
self._solver = solver
|
||||
self._verbose = verbose
|
||||
self._solver_options = solver_options if solver_options else {}
|
||||
self._map_bounds_to_constraints(weight_bounds)
|
||||
|
||||
def _map_bounds_to_constraints(self, test_bounds):
|
||||
"""
|
||||
@@ -218,8 +221,32 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
self._lower_bounds = np.nan_to_num(lower, nan=-1)
|
||||
self._upper_bounds = np.nan_to_num(upper, nan=1)
|
||||
|
||||
self._constraints.append(self._w >= self._lower_bounds)
|
||||
self._constraints.append(self._w <= self._upper_bounds)
|
||||
self.add_constraint(lambda w: w >= self._lower_bounds)
|
||||
self.add_constraint(lambda w: w <= self._upper_bounds)
|
||||
|
||||
def is_parameter_defined(self, parameter_name: str) -> bool:
|
||||
is_defined = False
|
||||
objective_and_constraints = self._constraints + [self._objective] if self._objective is not None else self._constraints
|
||||
for expr in objective_and_constraints:
|
||||
params = [arg for arg in get_all_args(expr) if isinstance(arg, cp.Parameter)]
|
||||
for param in params:
|
||||
if param.name() == parameter_name and not is_defined:
|
||||
is_defined = True
|
||||
elif param.name() == parameter_name and is_defined:
|
||||
raise Exception('Parameter name defined multiple times')
|
||||
return is_defined
|
||||
|
||||
def update_parameter_value(self, parameter_name: str, new_value: float) -> None:
|
||||
assert self.is_parameter_defined(parameter_name)
|
||||
was_updated = False
|
||||
objective_and_constraints = self._constraints + [self._objective] if self._objective is not None else self._constraints
|
||||
for expr in objective_and_constraints:
|
||||
params = [arg for arg in get_all_args(expr) if isinstance(arg, cp.Parameter)]
|
||||
for param in params:
|
||||
if param.name() == parameter_name:
|
||||
param.value = new_value
|
||||
was_updated = True
|
||||
assert was_updated
|
||||
|
||||
def _solve_cvxpy_opt_problem(self):
|
||||
"""
|
||||
@@ -229,14 +256,19 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
:raises exceptions.OptimizationError: if problem is not solvable by cvxpy
|
||||
"""
|
||||
try:
|
||||
self._opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
||||
|
||||
if self._solver is not None:
|
||||
self._opt.solve(
|
||||
solver=self._solver, verbose=self._verbose, **self._solver_options
|
||||
)
|
||||
if self._opt is None:
|
||||
self._opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
||||
self._initial_objective = self._objective.id
|
||||
self._initial_constraint_ids = {const.id for const in self._constraints}
|
||||
else:
|
||||
self._opt.solve(verbose=self._verbose, **self._solver_options)
|
||||
assert self._objective.id == self._initial_objective, \
|
||||
"The objective function was changed after the initial optimization. " \
|
||||
"Please create a new instance instead."
|
||||
assert {const.id for const in self._constraints} == self._initial_constraint_ids, \
|
||||
"The constraints were changed after the initial optimization. " \
|
||||
"Please create a new instance instead."
|
||||
self._opt.solve(solver=self._solver, verbose=self._verbose, **self._solver_options)
|
||||
|
||||
except (TypeError, cp.DCPError) as e:
|
||||
raise exceptions.OptimizationError from e
|
||||
|
||||
@@ -262,6 +294,9 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
:param new_objective: the objective to be added
|
||||
:type new_objective: cp.Expression (i.e function of cp.Variable)
|
||||
"""
|
||||
if self._opt is not None:
|
||||
raise Exception('Adding objectives to an already solved problem might have unintended consequences.'
|
||||
'A new instance should be created for the new set of objectives.')
|
||||
self._additional_objectives.append(new_objective(self._w, **kwargs))
|
||||
|
||||
def add_constraint(self, new_constraint):
|
||||
@@ -280,6 +315,9 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
"""
|
||||
if not callable(new_constraint):
|
||||
raise TypeError("New constraint must be provided as a lambda function")
|
||||
if self._opt is not None:
|
||||
raise Exception('Adding constraints to an already solved problem might have unintended consequences.'
|
||||
'A new instance should be created for the new set of constraints.')
|
||||
self._constraints.append(new_constraint(self._w))
|
||||
|
||||
def add_sector_constraints(self, sector_mapper, sector_lower, sector_upper):
|
||||
@@ -316,10 +354,10 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
)
|
||||
for sector in sector_upper:
|
||||
is_sector = [sector_mapper[t] == sector for t in self.tickers]
|
||||
self._constraints.append(cp.sum(self._w[is_sector]) <= sector_upper[sector])
|
||||
self.add_constraint(lambda w: cp.sum(w[is_sector]) <= sector_upper[sector])
|
||||
for sector in sector_lower:
|
||||
is_sector = [sector_mapper[t] == sector for t in self.tickers]
|
||||
self._constraints.append(cp.sum(self._w[is_sector]) >= sector_lower[sector])
|
||||
self.add_constraint(lambda w: cp.sum(w[is_sector]) >= sector_lower[sector])
|
||||
|
||||
def convex_objective(self, custom_objective, weights_sum_to_one=True, **kwargs):
|
||||
"""
|
||||
@@ -349,7 +387,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
self._objective += obj
|
||||
|
||||
if weights_sum_to_one:
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
self.add_constraint(lambda w: cp.sum(w) == 1)
|
||||
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
@@ -413,7 +451,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
# Construct constraints
|
||||
final_constraints = []
|
||||
if weights_sum_to_one:
|
||||
final_constraints.append({"type": "eq", "fun": lambda x: np.sum(x) - 1})
|
||||
final_constraints.append({"type": "eq", "fun": lambda w: np.sum(w) - 1})
|
||||
if constraints is not None:
|
||||
final_constraints += constraints
|
||||
|
||||
@@ -493,3 +531,18 @@ def portfolio_performance(
|
||||
if verbose:
|
||||
print("Annual volatility: {:.1f}%".format(100 * sigma))
|
||||
return None, sigma, None
|
||||
|
||||
|
||||
def get_all_args(expression: cp.Expression) -> List[cp.Expression]:
|
||||
if expression.args == []:
|
||||
return [expression]
|
||||
else:
|
||||
return list(flatten([get_all_args(arg) for arg in expression.args]))
|
||||
|
||||
|
||||
def flatten(l: Iterable) -> Iterable:
|
||||
for el in l:
|
||||
if isinstance(el, Iterable) and not isinstance(el, (str, bytes)):
|
||||
yield from flatten(el)
|
||||
else:
|
||||
yield el
|
||||
Reference in New Issue
Block a user