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 collections
|
||||||
import json
|
import json
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import cvxpy as cp
|
import cvxpy as cp
|
||||||
import scipy.optimize as sco
|
import scipy.optimize as sco
|
||||||
|
|
||||||
from . import objective_functions
|
from . import objective_functions
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
|
|
||||||
@@ -174,12 +178,11 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
self._constraints = []
|
self._constraints = []
|
||||||
self._lower_bounds = None
|
self._lower_bounds = None
|
||||||
self._upper_bounds = None
|
self._upper_bounds = None
|
||||||
self._map_bounds_to_constraints(weight_bounds)
|
|
||||||
|
|
||||||
self._opt = None
|
self._opt = None
|
||||||
self._solver = solver
|
self._solver = solver
|
||||||
self._verbose = verbose
|
self._verbose = verbose
|
||||||
self._solver_options = solver_options if solver_options else {}
|
self._solver_options = solver_options if solver_options else {}
|
||||||
|
self._map_bounds_to_constraints(weight_bounds)
|
||||||
|
|
||||||
def _map_bounds_to_constraints(self, test_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._lower_bounds = np.nan_to_num(lower, nan=-1)
|
||||||
self._upper_bounds = np.nan_to_num(upper, nan=1)
|
self._upper_bounds = np.nan_to_num(upper, nan=1)
|
||||||
|
|
||||||
self._constraints.append(self._w >= self._lower_bounds)
|
self.add_constraint(lambda w: w >= self._lower_bounds)
|
||||||
self._constraints.append(self._w <= self._upper_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):
|
def _solve_cvxpy_opt_problem(self):
|
||||||
"""
|
"""
|
||||||
@@ -229,14 +256,19 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
:raises exceptions.OptimizationError: if problem is not solvable by cvxpy
|
:raises exceptions.OptimizationError: if problem is not solvable by cvxpy
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if self._opt is None:
|
||||||
self._opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
self._opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
||||||
|
self._initial_objective = self._objective.id
|
||||||
if self._solver is not None:
|
self._initial_constraint_ids = {const.id for const in self._constraints}
|
||||||
self._opt.solve(
|
|
||||||
solver=self._solver, verbose=self._verbose, **self._solver_options
|
|
||||||
)
|
|
||||||
else:
|
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:
|
except (TypeError, cp.DCPError) as e:
|
||||||
raise exceptions.OptimizationError from e
|
raise exceptions.OptimizationError from e
|
||||||
|
|
||||||
@@ -262,6 +294,9 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
:param new_objective: the objective to be added
|
:param new_objective: the objective to be added
|
||||||
:type new_objective: cp.Expression (i.e function of cp.Variable)
|
: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))
|
self._additional_objectives.append(new_objective(self._w, **kwargs))
|
||||||
|
|
||||||
def add_constraint(self, new_constraint):
|
def add_constraint(self, new_constraint):
|
||||||
@@ -280,6 +315,9 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
"""
|
"""
|
||||||
if not callable(new_constraint):
|
if not callable(new_constraint):
|
||||||
raise TypeError("New constraint must be provided as a lambda function")
|
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))
|
self._constraints.append(new_constraint(self._w))
|
||||||
|
|
||||||
def add_sector_constraints(self, sector_mapper, sector_lower, sector_upper):
|
def add_sector_constraints(self, sector_mapper, sector_lower, sector_upper):
|
||||||
@@ -316,10 +354,10 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
)
|
)
|
||||||
for sector in sector_upper:
|
for sector in sector_upper:
|
||||||
is_sector = [sector_mapper[t] == sector for t in self.tickers]
|
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:
|
for sector in sector_lower:
|
||||||
is_sector = [sector_mapper[t] == sector for t in self.tickers]
|
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):
|
def convex_objective(self, custom_objective, weights_sum_to_one=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -349,7 +387,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
if weights_sum_to_one:
|
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()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -413,7 +451,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
# Construct constraints
|
# Construct constraints
|
||||||
final_constraints = []
|
final_constraints = []
|
||||||
if weights_sum_to_one:
|
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:
|
if constraints is not None:
|
||||||
final_constraints += constraints
|
final_constraints += constraints
|
||||||
|
|
||||||
@@ -493,3 +531,18 @@ def portfolio_performance(
|
|||||||
if verbose:
|
if verbose:
|
||||||
print("Annual volatility: {:.1f}%".format(100 * sigma))
|
print("Annual volatility: {:.1f}%".format(100 * sigma))
|
||||||
return None, sigma, None
|
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
|
||||||
@@ -125,14 +125,7 @@ class EfficientCDaR(EfficientFrontier):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints += [
|
self._add_cdar_constraints()
|
||||||
self._z >= self._u[1:] - self._alpha,
|
|
||||||
self._u[1:] >= self._u[:-1] - self.returns.values @ self._w,
|
|
||||||
self._u[0] == 0,
|
|
||||||
self._z >= 0,
|
|
||||||
self._u[1:] >= 0,
|
|
||||||
]
|
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -150,26 +143,17 @@ class EfficientCDaR(EfficientFrontier):
|
|||||||
:return: asset weights for the optimal portfolio
|
:return: asset weights for the optimal portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
"""
|
"""
|
||||||
self._objective = self._alpha + 1.0 / (
|
|
||||||
len(self.returns) * (1 - self._beta)
|
|
||||||
) * cp.sum(self._z)
|
|
||||||
|
|
||||||
for obj in self._additional_objectives:
|
update_existing_parameter = self.is_parameter_defined('target_return')
|
||||||
self._objective += obj
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
self._constraints += [
|
self.update_parameter_value('target_return', target_return)
|
||||||
self._z >= self._u[1:] - self._alpha,
|
|
||||||
self._u[1:] >= self._u[:-1] - self.returns.values @ self._w,
|
|
||||||
self._u[0] == 0,
|
|
||||||
self._z >= 0,
|
|
||||||
self._u[1:] >= 0,
|
|
||||||
]
|
|
||||||
|
|
||||||
ret = self.expected_returns.T @ self._w
|
|
||||||
self._constraints.append(ret >= target_return)
|
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
else:
|
||||||
|
ret = self.expected_returns.T @ self._w
|
||||||
|
target_return_par = cp.Parameter(value=target_return, name='target_return', nonneg=True)
|
||||||
|
self._constraints.append(ret >= target_return_par)
|
||||||
|
return self.min_cdar(market_neutral)
|
||||||
|
|
||||||
def efficient_risk(self, target_cdar, market_neutral=False):
|
def efficient_risk(self, target_cdar, market_neutral=False):
|
||||||
"""
|
"""
|
||||||
@@ -185,6 +169,12 @@ class EfficientCDaR(EfficientFrontier):
|
|||||||
:return: asset weights for the efficient risk portfolio
|
:return: asset weights for the efficient risk portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_cdar')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_cdar', target_cdar)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.portfolio_return(
|
self._objective = objective_functions.portfolio_return(
|
||||||
self._w, self.expected_returns
|
self._w, self.expected_returns
|
||||||
)
|
)
|
||||||
@@ -194,18 +184,21 @@ class EfficientCDaR(EfficientFrontier):
|
|||||||
cdar = self._alpha + 1.0 / (len(self.returns) * (1 - self._beta)) * cp.sum(
|
cdar = self._alpha + 1.0 / (len(self.returns) * (1 - self._beta)) * cp.sum(
|
||||||
self._z
|
self._z
|
||||||
)
|
)
|
||||||
self._constraints += [
|
target_cdar_par = cp.Parameter(value=target_cdar, name='target_cdar', nonneg=True)
|
||||||
cdar <= target_cdar,
|
self.add_constraint(lambda _: cdar <= target_cdar_par)
|
||||||
self._z >= self._u[1:] - self._alpha,
|
|
||||||
self._u[1:] >= self._u[:-1] - self.returns.values @ self._w,
|
self._add_cdar_constraints()
|
||||||
self._u[0] == 0,
|
|
||||||
self._z >= 0,
|
|
||||||
self._u[1:] >= 0,
|
|
||||||
]
|
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
|
def _add_cdar_constraints(self) -> None:
|
||||||
|
self.add_constraint(lambda _: self._z >= self._u[1:] - self._alpha)
|
||||||
|
self.add_constraint(lambda w: self._u[1:] >= self._u[:-1] - self.returns.values @ w)
|
||||||
|
self.add_constraint(lambda _: self._u[0] == 0)
|
||||||
|
self.add_constraint(lambda _: self._z >= 0)
|
||||||
|
self.add_constraint(lambda _: self._u[1:] >= 0)
|
||||||
|
|
||||||
def portfolio_performance(self, verbose=False):
|
def portfolio_performance(self, verbose=False):
|
||||||
"""
|
"""
|
||||||
After optimising, calculate (and optionally print) the performance of the optimal
|
After optimising, calculate (and optionally print) the performance of the optimal
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param expected_returns: expected returns for each asset. Can be None if
|
:param expected_returns: expected returns for each asset. Can be None if
|
||||||
optimising for semideviation only.
|
optimising for conditional value at risk only.
|
||||||
:type expected_returns: pd.Series, list, np.ndarray
|
:type expected_returns: pd.Series, list, np.ndarray
|
||||||
:param returns: (historic) returns for all your assets (no NaNs).
|
:param returns: (historic) returns for all your assets (no NaNs).
|
||||||
See ``expected_returns.returns_from_prices``.
|
See ``expected_returns.returns_from_prices``.
|
||||||
@@ -126,10 +126,8 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints += [
|
self.add_constraint(lambda _: self._u >= 0.0)
|
||||||
self._u >= 0.0,
|
self.add_constraint(lambda w: self.returns.values @ w + self._alpha + self._u >= 0.0)
|
||||||
self.returns.values @ self._w + self._alpha + self._u >= 0.0,
|
|
||||||
]
|
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
@@ -148,6 +146,11 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
:return: asset weights for the optimal portfolio
|
:return: asset weights for the optimal portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
"""
|
"""
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_return')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_return', target_return)
|
||||||
|
else:
|
||||||
self._objective = self._alpha + 1.0 / (
|
self._objective = self._alpha + 1.0 / (
|
||||||
len(self.returns) * (1 - self._beta)
|
len(self.returns) * (1 - self._beta)
|
||||||
) * cp.sum(self._u)
|
) * cp.sum(self._u)
|
||||||
@@ -155,13 +158,12 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints += [
|
self.add_constraint(lambda _: self._u >= 0.0)
|
||||||
self._u >= 0.0,
|
self.add_constraint(lambda w: self.returns.values @ w + self._alpha + self._u >= 0.0)
|
||||||
self.returns.values @ self._w + self._alpha + self._u >= 0.0,
|
|
||||||
]
|
|
||||||
|
|
||||||
ret = self.expected_returns.T @ self._w
|
ret = self.expected_returns.T @ self._w
|
||||||
self._constraints.append(ret >= target_return)
|
target_return_par = cp.Parameter(name='target_return', value=target_return)
|
||||||
|
self.add_constraint(lambda _: ret >= target_return_par)
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
@@ -172,7 +174,7 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
The resulting portfolio will have a CVaR less than the target
|
The resulting portfolio will have a CVaR less than the target
|
||||||
(but not guaranteed to be equal).
|
(but not guaranteed to be equal).
|
||||||
|
|
||||||
:param target_cvar: the desired maximum semideviation of the resulting portfolio.
|
:param target_cvar: the desired conditional value at risk of the resulting portfolio.
|
||||||
:type target_cvar: float
|
:type target_cvar: float
|
||||||
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
|
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
|
||||||
defaults to False. Requires negative lower weight bound.
|
defaults to False. Requires negative lower weight bound.
|
||||||
@@ -180,6 +182,11 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
:return: asset weights for the efficient risk portfolio
|
:return: asset weights for the efficient risk portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
"""
|
"""
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_cvar')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_cvar', target_cvar)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.portfolio_return(
|
self._objective = objective_functions.portfolio_return(
|
||||||
self._w, self.expected_returns
|
self._w, self.expected_returns
|
||||||
)
|
)
|
||||||
@@ -189,11 +196,11 @@ class EfficientCVaR(EfficientFrontier):
|
|||||||
cvar = self._alpha + 1.0 / (len(self.returns) * (1 - self._beta)) * cp.sum(
|
cvar = self._alpha + 1.0 / (len(self.returns) * (1 - self._beta)) * cp.sum(
|
||||||
self._u
|
self._u
|
||||||
)
|
)
|
||||||
self._constraints += [
|
target_cvar_par = cp.Parameter(value=target_cvar, name='target_cvar', nonneg=True)
|
||||||
cvar <= target_cvar,
|
|
||||||
self._u >= 0.0,
|
self.add_constraint(lambda _: cvar <= target_cvar_par)
|
||||||
self.returns.values @ self._w + self._alpha + self._u >= 0.0,
|
self.add_constraint(lambda _: self._u >= 0.0)
|
||||||
]
|
self.add_constraint(lambda w: self.returns.values @ w + self._alpha + self._u >= 0.0)
|
||||||
|
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
The ``efficient_frontier`` submodule houses the EfficientFrontier class, which generates
|
The ``efficient_frontier`` submodule houses the EfficientFrontier class, which generates
|
||||||
classical mean-variance optimal portfolios for a variety of objectives and constraints
|
classical mean-variance optimal portfolios for a variety of objectives and constraints
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import cvxpy as cp
|
import cvxpy as cp
|
||||||
@@ -86,6 +87,8 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
self.expected_returns = EfficientFrontier._validate_expected_returns(
|
self.expected_returns = EfficientFrontier._validate_expected_returns(
|
||||||
expected_returns
|
expected_returns
|
||||||
)
|
)
|
||||||
|
self._max_return_value = None
|
||||||
|
self._market_neutral = None
|
||||||
|
|
||||||
if self.expected_returns is None:
|
if self.expected_returns is None:
|
||||||
num_assets = len(cov_matrix)
|
num_assets = len(cov_matrix)
|
||||||
@@ -178,9 +181,11 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
del self._constraints[0]
|
del self._constraints[0]
|
||||||
del self._constraints[0]
|
del self._constraints[0]
|
||||||
|
|
||||||
self._constraints.append(cp.sum(self._w) == 0)
|
|
||||||
|
self.add_constraint(lambda w: cp.sum(w) == 0)
|
||||||
else:
|
else:
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
self.add_constraint(lambda w: cp.sum(w) == 1)
|
||||||
|
self._market_neutral = is_market_neutral
|
||||||
|
|
||||||
def min_volatility(self):
|
def min_volatility(self):
|
||||||
"""
|
"""
|
||||||
@@ -195,7 +200,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
self.add_constraint(lambda w: cp.sum(w) == 1)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
def _max_return(self, return_value=True):
|
def _max_return(self, return_value=True):
|
||||||
@@ -212,13 +217,10 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
self._w, self.expected_returns
|
self._w, self.expected_returns
|
||||||
)
|
)
|
||||||
|
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
self.add_constraint(lambda w: cp.sum(w) == 1)
|
||||||
|
|
||||||
res = self._solve_cvxpy_opt_problem()
|
res = self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
# Cleanup constraints since this is a helper method
|
|
||||||
del self._constraints[-1]
|
|
||||||
|
|
||||||
if return_value:
|
if return_value:
|
||||||
return -self._opt.value
|
return -self._opt.value
|
||||||
else:
|
else:
|
||||||
@@ -308,6 +310,11 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
if risk_aversion <= 0:
|
if risk_aversion <= 0:
|
||||||
raise ValueError("risk aversion coefficient must be greater than zero")
|
raise ValueError("risk aversion coefficient must be greater than zero")
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('risk_aversion')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('risk_aversion', risk_aversion)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.quadratic_utility(
|
self._objective = objective_functions.quadratic_utility(
|
||||||
self._w, self.expected_returns, self.cov_matrix, risk_aversion=risk_aversion
|
self._w, self.expected_returns, self.cov_matrix, risk_aversion=risk_aversion
|
||||||
)
|
)
|
||||||
@@ -345,6 +352,11 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_variance')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_variance', target_volatility ** 2)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.portfolio_return(
|
self._objective = objective_functions.portfolio_return(
|
||||||
self._w, self.expected_returns
|
self._w, self.expected_returns
|
||||||
)
|
)
|
||||||
@@ -353,7 +365,8 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints.append(variance <= target_volatility ** 2)
|
target_variance = cp.Parameter(name="target_variance", value=target_volatility ** 2, nonneg=True)
|
||||||
|
self.add_constraint(lambda _: variance <= target_variance)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -373,11 +386,18 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(target_return, float) or target_return < 0:
|
if not isinstance(target_return, float) or target_return < 0:
|
||||||
raise ValueError("target_return should be a positive float")
|
raise ValueError("target_return should be a positive float")
|
||||||
if target_return > self._max_return():
|
if not self._max_return_value:
|
||||||
|
self._max_return_value = copy.deepcopy(self)._max_return()
|
||||||
|
if target_return > self._max_return_value:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"target_return must be lower than the maximum possible return"
|
"target_return must be lower than the maximum possible return"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_return')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_return', target_return)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.portfolio_variance(
|
self._objective = objective_functions.portfolio_variance(
|
||||||
self._w, self.cov_matrix
|
self._w, self.cov_matrix
|
||||||
)
|
)
|
||||||
@@ -388,7 +408,8 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints.append(ret >= target_return)
|
target_return_par = cp.Parameter(name='target_return', value=target_return)
|
||||||
|
self.add_constraint(lambda _: ret >= target_return_par)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -423,3 +444,6 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
verbose,
|
verbose,
|
||||||
risk_free_rate,
|
risk_free_rate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _validate_market_neutral(self, market_neutral: bool) -> None:
|
||||||
|
assert self._market_neutral == market_neutral, 'A new instance must be created when changing market_neutral'
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class EfficientSemivariance(EfficientFrontier):
|
|||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
||||||
self._constraints.append(B @ self._w - p + n == 0)
|
self.add_constraint(lambda w: B @ w - p + n == 0)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -146,16 +146,22 @@ class EfficientSemivariance(EfficientFrontier):
|
|||||||
if risk_aversion <= 0:
|
if risk_aversion <= 0:
|
||||||
raise ValueError("risk aversion coefficient must be greater than zero")
|
raise ValueError("risk aversion coefficient must be greater than zero")
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('risk_aversion')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('risk_aversion', risk_aversion)
|
||||||
|
else:
|
||||||
p = cp.Variable(self._T, nonneg=True)
|
p = cp.Variable(self._T, nonneg=True)
|
||||||
n = cp.Variable(self._T, nonneg=True)
|
n = cp.Variable(self._T, nonneg=True)
|
||||||
mu = objective_functions.portfolio_return(self._w, self.expected_returns)
|
mu = objective_functions.portfolio_return(self._w, self.expected_returns)
|
||||||
mu /= self.frequency
|
mu /= self.frequency
|
||||||
self._objective = mu + 0.5 * risk_aversion * cp.sum(cp.square(n))
|
risk_aversion_par = cp.Parameter(value=risk_aversion, name='risk_aversion', nonneg=True)
|
||||||
|
self._objective = mu + 0.5 * risk_aversion_par * cp.sum(cp.square(n))
|
||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
||||||
self._constraints.append(B @ self._w - p + n == 0)
|
self.add_constraint(lambda w: B @ w - p + n == 0)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -173,6 +179,11 @@ class EfficientSemivariance(EfficientFrontier):
|
|||||||
:return: asset weights for the efficient risk portfolio
|
:return: asset weights for the efficient risk portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
"""
|
"""
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_semivariance')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_semivariance', target_semideviation ** 2)
|
||||||
|
else:
|
||||||
self._objective = objective_functions.portfolio_return(
|
self._objective = objective_functions.portfolio_return(
|
||||||
self._w, self.expected_returns
|
self._w, self.expected_returns
|
||||||
)
|
)
|
||||||
@@ -182,11 +193,10 @@ class EfficientSemivariance(EfficientFrontier):
|
|||||||
p = cp.Variable(self._T, nonneg=True)
|
p = cp.Variable(self._T, nonneg=True)
|
||||||
n = cp.Variable(self._T, nonneg=True)
|
n = cp.Variable(self._T, nonneg=True)
|
||||||
|
|
||||||
self._constraints.append(
|
target_semivariance = cp.Parameter(value=target_semideviation**2, name='target_semivariance', nonneg=True)
|
||||||
self.frequency * cp.sum(cp.square(n)) <= (target_semideviation ** 2)
|
self.add_constraint(lambda _: self.frequency * cp.sum(cp.square(n)) <= target_semivariance)
|
||||||
)
|
|
||||||
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
||||||
self._constraints.append(B @ self._w - p + n == 0)
|
self.add_constraint(lambda w: B @ w - p + n == 0)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
@@ -210,17 +220,23 @@ class EfficientSemivariance(EfficientFrontier):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"target_return must be lower than the largest expected return"
|
"target_return must be lower than the largest expected return"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_existing_parameter = self.is_parameter_defined('target_return')
|
||||||
|
if update_existing_parameter:
|
||||||
|
self._validate_market_neutral(market_neutral)
|
||||||
|
self.update_parameter_value('target_return', target_return)
|
||||||
|
else:
|
||||||
|
|
||||||
p = cp.Variable(self._T, nonneg=True)
|
p = cp.Variable(self._T, nonneg=True)
|
||||||
n = cp.Variable(self._T, nonneg=True)
|
n = cp.Variable(self._T, nonneg=True)
|
||||||
self._objective = cp.sum(cp.square(n))
|
self._objective = cp.sum(cp.square(n))
|
||||||
for obj in self._additional_objectives:
|
for obj in self._additional_objectives:
|
||||||
self._objective += obj
|
self._objective += obj
|
||||||
|
|
||||||
self._constraints.append(
|
target_return_par = cp.Parameter(name='target_return', value=target_return)
|
||||||
cp.sum(self._w @ self.expected_returns) >= target_return
|
self.add_constraint(lambda w: cp.sum(w @ self.expected_returns) >= target_return_par)
|
||||||
)
|
|
||||||
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
|
||||||
self._constraints.append(B @ self._w - p + n == 0)
|
self.add_constraint(lambda w: B @ w - p + n == 0)
|
||||||
self._make_weight_sum_constraint(market_neutral)
|
self._make_weight_sum_constraint(market_neutral)
|
||||||
return self._solve_cvxpy_opt_problem()
|
return self._solve_cvxpy_opt_problem()
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ def quadratic_utility(w, expected_returns, cov_matrix, risk_aversion, negative=T
|
|||||||
mu = w @ expected_returns
|
mu = w @ expected_returns
|
||||||
variance = cp.quad_form(w, cov_matrix)
|
variance = cp.quad_form(w, cov_matrix)
|
||||||
|
|
||||||
utility = mu - 0.5 * risk_aversion * variance
|
risk_aversion_par = cp.Parameter(value=risk_aversion, name='risk_aversion', nonneg=True)
|
||||||
|
utility = mu - 0.5 * risk_aversion_par * variance
|
||||||
return _objective_value(w, sign * utility)
|
return _objective_value(w, sign * utility)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -168,15 +168,13 @@ def _plot_ef(ef, ef_param, ef_param_range, ax, show_assets):
|
|||||||
|
|
||||||
# Create a portfolio for each value of ef_param_range
|
# Create a portfolio for each value of ef_param_range
|
||||||
for param_value in ef_param_range:
|
for param_value in ef_param_range:
|
||||||
ef_i = copy.deepcopy(ef)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if ef_param == "utility":
|
if ef_param == "utility":
|
||||||
ef_i.max_quadratic_utility(param_value)
|
ef.max_quadratic_utility(param_value)
|
||||||
elif ef_param == "risk":
|
elif ef_param == "risk":
|
||||||
ef_i.efficient_risk(param_value)
|
ef.efficient_risk(param_value)
|
||||||
elif ef_param == "return":
|
elif ef_param == "return":
|
||||||
ef_i.efficient_return(param_value)
|
ef.efficient_return(param_value)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"ef_param should be one of {'utility', 'risk', 'return'}"
|
"ef_param should be one of {'utility', 'risk', 'return'}"
|
||||||
@@ -190,7 +188,7 @@ def _plot_ef(ef, ef_param, ef_param_range, ax, show_assets):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ret, sigma, _ = ef_i.portfolio_performance()
|
ret, sigma, _ = ef.portfolio_performance()
|
||||||
mus.append(ret)
|
mus.append(ret)
|
||||||
sigmas.append(sigma)
|
sigmas.append(sigma)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pandas as pd
|
|||||||
import pytest
|
import pytest
|
||||||
import cvxpy as cp
|
import cvxpy as cp
|
||||||
|
|
||||||
from pypfopt import EfficientFrontier
|
from pypfopt import EfficientFrontier, objective_functions
|
||||||
from pypfopt import exceptions
|
from pypfopt import exceptions
|
||||||
from pypfopt.base_optimizer import portfolio_performance, BaseOptimizer
|
from pypfopt.base_optimizer import portfolio_performance, BaseOptimizer
|
||||||
from tests.utilities_for_tests import get_data, setup_efficient_frontier
|
from tests.utilities_for_tests import get_data, setup_efficient_frontier
|
||||||
@@ -56,7 +56,10 @@ def test_weight_bounds_minus_one_to_one():
|
|||||||
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
|
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
|
||||||
)
|
)
|
||||||
assert ef.max_sharpe()
|
assert ef.max_sharpe()
|
||||||
assert ef.min_volatility()
|
ef2 = EfficientFrontier(
|
||||||
|
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
|
||||||
|
)
|
||||||
|
assert ef2.min_volatility()
|
||||||
|
|
||||||
|
|
||||||
def test_none_bounds():
|
def test_none_bounds():
|
||||||
@@ -189,12 +192,13 @@ def test_efficient_frontier_init_errors():
|
|||||||
|
|
||||||
|
|
||||||
def test_set_weights():
|
def test_set_weights():
|
||||||
ef = setup_efficient_frontier()
|
ef1 = setup_efficient_frontier()
|
||||||
w1 = ef.min_volatility()
|
w1 = ef1.min_volatility()
|
||||||
test_weights = ef.weights
|
test_weights = ef1.weights
|
||||||
ef.min_volatility()
|
ef2 = setup_efficient_frontier()
|
||||||
ef.set_weights(w1)
|
ef2.min_volatility()
|
||||||
np.testing.assert_array_almost_equal(test_weights, ef.weights)
|
ef2.set_weights(w1)
|
||||||
|
np.testing.assert_array_almost_equal(test_weights, ef2.weights)
|
||||||
|
|
||||||
|
|
||||||
def test_save_weights_to_file():
|
def test_save_weights_to_file():
|
||||||
@@ -287,3 +291,38 @@ def test_problem_access():
|
|||||||
ef = setup_efficient_frontier()
|
ef = setup_efficient_frontier()
|
||||||
ef.max_sharpe()
|
ef.max_sharpe()
|
||||||
assert isinstance(ef._opt, cp.Problem)
|
assert isinstance(ef._opt, cp.Problem)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_immutability():
|
||||||
|
ef = setup_efficient_frontier()
|
||||||
|
ef.efficient_return(0.2)
|
||||||
|
|
||||||
|
with pytest.raises(Exception,
|
||||||
|
match='Adding constraints to an already solved problem might have unintended consequences'):
|
||||||
|
ef.min_volatility()
|
||||||
|
|
||||||
|
ef = setup_efficient_frontier()
|
||||||
|
ef.efficient_return(0.2)
|
||||||
|
with pytest.raises(Exception,
|
||||||
|
match='Adding constraints to an already solved problem might have unintended consequences'):
|
||||||
|
ef.add_constraint(lambda w: w >= 0.1)
|
||||||
|
|
||||||
|
ef = setup_efficient_frontier()
|
||||||
|
ef.efficient_return(0.2)
|
||||||
|
prev_w = np.array([1 / ef.n_assets] * ef.n_assets)
|
||||||
|
with pytest.raises(Exception,
|
||||||
|
match='Adding objectives to an already solved problem might have unintended consequences'):
|
||||||
|
ef.add_objective(objective_functions.transaction_cost, w_prev=prev_w)
|
||||||
|
|
||||||
|
ef = setup_efficient_frontier()
|
||||||
|
ef.efficient_return(0.2)
|
||||||
|
ef._constraints += [ef._w >= 0.1]
|
||||||
|
with pytest.raises(Exception,
|
||||||
|
match='The constraints were changed after the initial optimization'):
|
||||||
|
ef.efficient_return(0.2)
|
||||||
|
|
||||||
|
ef = setup_efficient_frontier()
|
||||||
|
ef.efficient_return(0.2, market_neutral=True)
|
||||||
|
with pytest.raises(Exception,
|
||||||
|
match='A new instance must be created when changing market_neutral'):
|
||||||
|
ef.efficient_return(0.2, market_neutral=False)
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ def test_custom_convex_abs_exposure():
|
|||||||
)
|
)
|
||||||
|
|
||||||
ef.add_constraint(lambda x: cp.norm(x, 1) <= 2)
|
ef.add_constraint(lambda x: cp.norm(x, 1) <= 2)
|
||||||
ef.min_volatility()
|
|
||||||
ef.convex_objective(
|
ef.convex_objective(
|
||||||
objective_functions.portfolio_variance,
|
objective_functions.portfolio_variance,
|
||||||
cov_matrix=ef.cov_matrix,
|
cov_matrix=ef.cov_matrix,
|
||||||
|
|||||||
@@ -350,15 +350,14 @@ def test_efficient_risk_L2_reg():
|
|||||||
atol=1e-4,
|
atol=1e-4,
|
||||||
)
|
)
|
||||||
|
|
||||||
ef2 = setup_efficient_cdar()
|
cd2 = setup_efficient_cdar()
|
||||||
cd.add_objective(objective_functions.L2_reg, gamma=1)
|
cd2.efficient_risk(0.18)
|
||||||
ef2.efficient_risk(0.18)
|
|
||||||
|
|
||||||
# L2_reg should pull close to equal weight
|
# L2_reg should pull close to equal weight
|
||||||
equal_weight = np.full((cd.n_assets,), 1 / cd.n_assets)
|
equal_weight = np.full((cd.n_assets,), 1 / cd.n_assets)
|
||||||
assert (
|
assert (
|
||||||
np.abs(equal_weight - cd.weights).sum()
|
np.abs(equal_weight - cd.weights).sum()
|
||||||
< np.abs(equal_weight - ef2.weights).sum()
|
< np.abs(equal_weight - cd2.weights).sum()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -455,3 +454,13 @@ def test_cdar_errors():
|
|||||||
historical_rets = historical_rets.iloc[:, :-1]
|
historical_rets = historical_rets.iloc[:, :-1]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
EfficientCDaR(mu, historical_rets)
|
EfficientCDaR(mu, historical_rets)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parametrization():
|
||||||
|
cd = setup_efficient_cdar()
|
||||||
|
cd.efficient_risk(0.08)
|
||||||
|
cd.efficient_risk(0.07)
|
||||||
|
|
||||||
|
cd = setup_efficient_cdar()
|
||||||
|
cd.efficient_return(0.08)
|
||||||
|
cd.efficient_return(0.07)
|
||||||
|
|||||||
@@ -348,15 +348,14 @@ def test_efficient_risk_L2_reg():
|
|||||||
atol=1e-4,
|
atol=1e-4,
|
||||||
)
|
)
|
||||||
|
|
||||||
ef2 = setup_efficient_cvar()
|
cv2 = setup_efficient_cvar()
|
||||||
cv.add_objective(objective_functions.L2_reg, gamma=1)
|
cv2.efficient_risk(0.19)
|
||||||
ef2.efficient_risk(0.03)
|
|
||||||
|
|
||||||
# L2_reg should pull close to equal weight
|
# L2_reg should pull close to equal weight
|
||||||
equal_weight = np.full((cv.n_assets,), 1 / cv.n_assets)
|
equal_weight = np.full((cv.n_assets,), 1 / cv.n_assets)
|
||||||
assert (
|
assert (
|
||||||
np.abs(equal_weight - cv.weights).sum()
|
np.abs(equal_weight - cv.weights).sum()
|
||||||
< np.abs(equal_weight - ef2.weights).sum()
|
< np.abs(equal_weight - cv2.weights).sum()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -456,3 +455,13 @@ def test_cvar_errors():
|
|||||||
historical_rets = historical_rets.iloc[:, :-1]
|
historical_rets = historical_rets.iloc[:, :-1]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
EfficientCVaR(mu, historical_rets)
|
EfficientCVaR(mu, historical_rets)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parametrization():
|
||||||
|
cv = setup_efficient_cvar()
|
||||||
|
cv.efficient_risk(0.19)
|
||||||
|
cv.efficient_risk(0.19)
|
||||||
|
|
||||||
|
cv = setup_efficient_cvar()
|
||||||
|
cv.efficient_return(0.25)
|
||||||
|
cv.efficient_return(0.25)
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ def test_min_volatility_L2_reg_many_values():
|
|||||||
ef.min_volatility()
|
ef.min_volatility()
|
||||||
# Count the number of weights more 1%
|
# Count the number of weights more 1%
|
||||||
initial_number = sum(ef.weights > 0.01)
|
initial_number = sum(ef.weights > 0.01)
|
||||||
for _ in range(10):
|
for gamma_multiplier in range(1, 10):
|
||||||
ef.add_objective(objective_functions.L2_reg, gamma=0.05)
|
ef = setup_efficient_frontier()
|
||||||
|
ef.add_objective(objective_functions.L2_reg, gamma=0.05*gamma_multiplier)
|
||||||
ef.min_volatility()
|
ef.min_volatility()
|
||||||
np.testing.assert_almost_equal(ef.weights.sum(), 1)
|
np.testing.assert_almost_equal(ef.weights.sum(), 1)
|
||||||
new_number = sum(ef.weights > 0.01)
|
new_number = sum(ef.weights > 0.01)
|
||||||
@@ -388,7 +389,7 @@ def test_max_sharpe_error():
|
|||||||
|
|
||||||
# An unsupported constraint type, which is incidentally meaningless.
|
# An unsupported constraint type, which is incidentally meaningless.
|
||||||
v = cp.Variable((2, 2), PSD=True)
|
v = cp.Variable((2, 2), PSD=True)
|
||||||
ef._constraints.append(v >> np.zeros((2, 2)))
|
ef.add_constraint(lambda _: v >> np.zeros((2, 2)))
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
ef.max_sharpe()
|
ef.max_sharpe()
|
||||||
|
|
||||||
@@ -824,8 +825,9 @@ def test_max_quadratic_utility():
|
|||||||
|
|
||||||
ret1, var1, _ = ef.portfolio_performance()
|
ret1, var1, _ = ef.portfolio_performance()
|
||||||
# increasing risk_aversion should lower both vol and return
|
# increasing risk_aversion should lower both vol and return
|
||||||
ef.max_quadratic_utility(10)
|
ef2 = setup_efficient_frontier()
|
||||||
ret2, var2, _ = ef.portfolio_performance()
|
ef2.max_quadratic_utility(10)
|
||||||
|
ret2, var2, _ = ef2.portfolio_performance()
|
||||||
assert ret2 < ret1 and var2 < var1
|
assert ret2 < ret1 and var2 < var1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -465,7 +465,6 @@ def test_efficient_risk_L2_reg():
|
|||||||
)
|
)
|
||||||
|
|
||||||
ef2 = setup_efficient_semivariance()
|
ef2 = setup_efficient_semivariance()
|
||||||
es.add_objective(objective_functions.L2_reg, gamma=1)
|
|
||||||
ef2.efficient_risk(0.19)
|
ef2.efficient_risk(0.19)
|
||||||
|
|
||||||
# L2_reg should pull close to equal weight
|
# L2_reg should pull close to equal weight
|
||||||
@@ -582,3 +581,17 @@ def test_efficient_semivariance_vs_heuristic_weekly():
|
|||||||
|
|
||||||
assert semi_deviation < semi_deviation_ef
|
assert semi_deviation < semi_deviation_ef
|
||||||
assert mu_es / semi_deviation > mu_ef / semi_deviation_ef
|
assert mu_es / semi_deviation > mu_ef / semi_deviation_ef
|
||||||
|
|
||||||
|
|
||||||
|
def test_parametrization():
|
||||||
|
es = setup_efficient_semivariance()
|
||||||
|
es.efficient_risk(0.19)
|
||||||
|
es.efficient_risk(0.19)
|
||||||
|
|
||||||
|
es = setup_efficient_semivariance()
|
||||||
|
es.efficient_return(0.25)
|
||||||
|
es.efficient_return(0.25)
|
||||||
|
|
||||||
|
es = setup_efficient_semivariance()
|
||||||
|
es.max_quadratic_utility(1)
|
||||||
|
es.max_quadratic_utility(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user