def set_constraints(self, constraints: ConstraintMap, eps: float): assert self.x is not None for name, f in constraints.inequality.items(): value = abs(f(self.x)) if value <= eps: self.tight_hin.append(name) elif value > eps: self.violations.append(name) for name, f in constraints.equality.items(): if abs(f(self.x)) > eps: self.violations.append(name) for eq, c_map in [("<=", constraints.inequality), ("=", constraints.equality)]: for name, f in c_map.items(): if len(inspect.signature(f).parameters) == 1: v = f(self.x) / get_option("C.SCALE") else: # at most 2 parameters v = f(self.x, np.ones( (len(self.x), len(self.x)))) / get_option("C.SCALE") self.constraint_values.append({ "Name": name, "Equality": eq, "Value": v })
def __init__(self, mb: ModelBuilder, solution: np.ndarray, scenario_solutions: np.ndarray, proportions: Optional[np.ndarray], dist_func: Callable[[np.ndarray], np.ndarray], probability: np.ndarray, eps: float = get_option("EPS.CONSTRAINT")): self.num_assets = mb.num_assets self.num_scenarios = mb.num_scenarios self.solution = np.asarray(solution) self.proportions = np.asarray( proportions) if proportions is not None else None self.scenario_solutions = np.asarray(scenario_solutions) self.probability = probability self._assets = [f"Asset_{i + 1}" for i in range(mb.num_assets)] self._scenarios = [ f"Scenario_{i + 1}" for i in range(mb.num_scenarios) ] self.tight_constraint: List[str] = [] self.violations: List[str] = [] self._check_matrix_constraints(mb.constraints, eps) self._check_functional_constraints(mb.constraints, eps) self.scenario_objective_values = self._derive_scenario_objective_values( mb) self.regret_value = self._derive_regret_value(mb, dist_func) self.constraint_values = self._derive_constraint_values(mb)
def scenario_solution_equal_or_better(obj_funcs, solutions, expected): results = [] for f, w, t, in zip(obj_funcs, solutions, expected): diff = (f(w) - f(t)) / get_option("F.SCALE") results.append(round(diff, 3) >= 0) return np.alltrue(results)
def _derive_regret_value( self, mb: ModelBuilder, dist_func: Callable[[np.ndarray], np.ndarray]) -> float: f_values = np.array( [f(s) for f, s in zip(mb.obj_funcs, self.scenario_solutions)]) curr_f_values = np.array([f(self.solution) for f in mb.obj_funcs]) cost = dist_func(f_values - curr_f_values) / get_option("F.SCALE") return sum(self.probability * cost)
def _derive_scenario_objective_values(self, mb: ModelBuilder): values = [] for f, s in zip(mb.obj_funcs, self.scenario_solutions): if len(inspect.signature(f).parameters) == 1: v = f(s) else: # number of parameters can only be 2 in this case grad = np.ones( (self.num_assets, self.num_assets)) # filler gradient, not necessary v = f(s, grad) values.append(v / get_option("F.SCALE")) return np.array(values)
def add_inequality_matrix_constraint(self, A, b): r""" Sets inequality constraints in standard matrix form. For inequality, :math:`\mathbf{A} \cdot \mathbf{x} \leq \mathbf{b}` Parameters ---------- A: {iterable float, ndarray} Inequality matrix. Must be 2 dimensional. b: {scalar, ndarray} Inequality vector or scalar. If scalar, it will be propagated. """ s = get_option("C.SCALE") self._mb.add_inequality_matrix_constraints(A * s, b * s) return self
def add_equality_matrix_constraint(self, Aeq, beq): r""" Sets equality constraints in standard matrix form. For equality, :math:`\mathbf{A} \cdot \mathbf{x} = \mathbf{b}` Parameters ---------- Aeq: {iterable float, ndarray} Equality matrix. Must be 2 dimensional beq: {scalar, ndarray} Equality vector or scalar. If scalar, it will be propagated """ s = get_option("C.SCALE") self._mb.add_equality_matrix_constraints(Aeq * s, beq * s) return self
def __init__(self, n: int, algorithm=LD_SLSQP, *args, **kwargs): """ The BaseOptimizer is the raw optimizer with minimal support. For advanced users, this class will provide the most flexibility. The default algorithm used is Sequential Least Squares Quadratic Programming. Parameters ---------- n: int number of assets algorithm: int or str the optimization algorithm args other arguments to setup the optimizer kwargs other keyword arguments """ if isinstance(algorithm, str): algorithm = map_algorithm(algorithm) self._n = n self._model = nl.opt(algorithm, n, *args) has_grad = has_gradient(algorithm) if has_grad == 'NOT COMPILED': raise NotImplementedError(f"Cannot use '{nl.algorithm_name(algorithm)}' as it is not compiled") self._auto_grad: bool = kwargs.get('auto_grad', has_grad) self._eps = get_option('EPS.STEP') self._c_eps = get_option('EPS.CONSTRAINT') self.set_xtol_abs(get_option('EPS.X_ABS')) self.set_xtol_rel(get_option('EPS.X_REL')) self.set_ftol_abs(get_option('EPS.F_ABS')) self.set_ftol_rel(get_option('EPS.F_REL')) self.set_maxeval(get_option('MAX.EVAL')) self._cmap = ConstraintMap() self._result = Result() self._max_or_min = None self._verbose = kwargs.get('verbose', False)
def _derive_constraint_values(self, mb: ModelBuilder): constraints = [] for eq, constraint_map in [("<=", mb.constraints.m_inequality), ("<=", mb.constraints.inequality), ("=", mb.constraints.m_equality), ("=", mb.constraints.equality)]: for name, fns in constraint_map.items(): for f, s in zip(fns, self.scenarios): if len(inspect.signature(f).parameters) == 1: v = f(self.solution) else: # number of parameters can only be 2 in this case grad = np.ones(( self.num_assets, self.num_assets)) # filler gradient, not necessary v = f(self.solution, grad) constraints.append({ "Name": name, "Scenario": s, "Equality": eq, "Value": v / get_option("C.SCALE") }) return constraints
def objective(w): return (self.cvar_data.cvar(w, self.rebalance, percentile) - self.penalty(w)) * get_option("F.SCALE")
def add_equality_matrix_constraint(self, Aeq, beq, tol=None): s = get_option("C.SCALE") return super().add_equality_matrix_constraint(Aeq * s, beq * s, tol)
def objective(w): return (self.data.sharpe_ratio(w, self.rebalance) - self.penalty(w)) * get_option("F.SCALE")
def objective(w): return (self.data.volatility(w) + self.penalty(w)) * get_option("F.SCALE")
def _ctr_min_returns(w): return get_option("C.SCALE") * ( min_ret - self.data.expected_return(w, self.rebalance))
def objective(w): w = format_weights(w, True) return (d.sharpe_ratio(w, self.rebalance) - self.penalties[i](w)) * get_option("F.SCALE")
def objective(w): return (d.sharpe_ratio(w, self.rebalance) - self.penalties[i](w)) * get_option("F.SCALE")
def objective(w): w = format_weights(w, is_tracking_error) return (d.volatility(w) + self.penalties[i](w)) * get_option("F.SCALE")
def add_inequality_matrix_constraint(self, A, b, tol=None): s = get_option("C.SCALE") return super().add_inequality_matrix_constraint(A * s, b * s, tol)
def objective(w): w = format_weights(w, active_cvar) return (d.cvar(w, self.rebalance, percentile) - self.penalties[i](w)) * get_option("F.SCALE")
def constraint(w): w = format_weights(w, as_active_returns) return get_option("C.SCALE") * ( ret - d.expected_return(w, self.rebalance))
def __init__(self, num_assets: int, num_scenarios: int, prob: OptArray = None, algorithm=LD_SLSQP, c_eps: Optional[float] = None, xtol_abs: Union[float, np.ndarray, None] = None, xtol_rel: Union[float, np.ndarray, None] = None, ftol_abs: Optional[float] = None, ftol_rel: Optional[float] = None, max_eval: Optional[int] = None, verbose=False, sum_to_1=True, max_attempts=5): r""" The RegretOptimizer is a convenience class for scenario based optimization. Notes ----- The term regret refers to the instance where after having decided on one alternative, the choice of a different alternative would have led to a more optimal (better) outcome when the eventual scenario transpires. The RegretOptimizer employs a 2 stage optimization process. In the first step, the optimizer calculates the optimal weights for each scenario. In the second stage, the optimizer minimizes the regret function to give the final optimal portfolio weights. Assuming the objective is to maximize returns subject to some volatility constraints, the first stage optimization will be as listed .. math:: \begin{gather*} \underset{w_s}{\max} R_s(w_s) \forall s \in S \\ s.t. \\ \sigma_s(w_s) \leq \Sigma \end{gather*} where :math:`R_s(\cdot)` is the returns function for scenario :math:`s`, :math:`\sigma_s(\cdot)` is the volatility function for scenario :math:`s` and :math:`\Sigma` is the volatility threshold. Subsequently, to minimize the regret across all scenarios, :math:`S`, .. math:: \begin{gather*} \underset{w}{\min} \sum_{s \in S} p_s \cdot D(R_s(w_s) - R_s(w)) \end{gather*} Where :math:`D(\cdot)` is a distance function (usually quadratic) and :math:`p_s` is the discrete probability of scenario :math:`s` occurring. Parameters ---------- num_assets: int Number of assets num_scenarios: int Number of scenarios prob Vector containing probability of each scenario occurring algorithm The optimization algorithm. Default algorithm is Sequential Least Squares Quadratic Programming. c_eps: float, optional Constraint epsilon is the tolerance for the inequality and equality constraints functions. Any value that is less than the constraint epsilon is considered to be within the boundary. xtol_abs: float or np.ndarray, optional Set absolute tolerances on optimization parameters. :code:`tol` is an array giving the tolerances: stop when an optimization step (or an estimate of the optimum) changes every parameter :code:`x[i]` by less than :code:`tol[i]`. For convenience, if a scalar :code:`tol` is given, it will be used to set the absolute tolerances in all n optimization parameters to the same value. Criterion is disabled if tol is non-positive. xtol_rel: float or np.ndarray, optional Set relative tolerance on optimization parameters: stop when an optimization step (or an estimate of the optimum) causes a relative change the parameters :code:`x` by less than :code:`tol`, i.e. :math:`\|\Delta x\|_w < tol \cdot \|x\|_w` measured by a weighted :math:`L_1` norm :math:`\|x\|_w = \sum_i w_i |x_i|`, where the weights :math:`w_i` default to 1. (If there is any chance that the optimal :math:`\|x\|` is close to zero, you might want to set an absolute tolerance with `code:`xtol_abs` as well.) Criterion is disabled if tol is non-positive. ftol_abs: float, optional Set absolute tolerance on function value: stop when an optimization step (or an estimate of the optimum) changes the function value by less than :code:`tol`. Criterion is disabled if tol is non-positive. ftol_rel: float, optional Set relative tolerance on function value: stop when an optimization step (or an estimate of the optimum) changes the objective function value by less than :code:`tol` multiplied by the absolute value of the function value. (If there is any chance that your optimum function value is close to zero, you might want to set an absolute tolerance with :code:`ftol_abs` as well.) Criterion is disabled if tol is non-positive. max_eval: int, optional Stop when the number of function evaluations exceeds :code:`maxeval`. (This is not a strict maximum: the number of function evaluations may exceed :code:`maxeval` slightly, depending upon the algorithm.) Criterion is disabled if maxeval is non-positive. verbose: bool If True, the optimizer will report its operations sum_to_1: bool If true, the optimal weights for each first level scenario must sum to 1. max_attempts: int Number of times to retry optimization. This is useful when optimization is in a highly unstable or non-convex space. See Also -------- :class:`DiscreteUncertaintyOptimizer`: Discrete Uncertainty Optimizer """ assert isinstance( num_assets, int ) and num_assets > 0, "num_assets must be an integer and more than 0" assert isinstance( num_scenarios, int ) and num_scenarios > 0, "num_assets must be an integer and more than 0" self._num_assets = num_assets self._num_scenarios = num_scenarios self._mb = ModelBuilder( num_assets, num_scenarios, algorithm, sum_to_1, validate_tolerance(xtol_abs or get_option('EPS.X_ABS')), validate_tolerance(xtol_rel or get_option('EPS.X_REL')), validate_tolerance(ftol_abs or get_option('EPS.F_ABS')), validate_tolerance(ftol_rel or get_option('EPS.F_REL')), int(max_eval or get_option('MAX.EVAL')), c_eps or get_option('EPS.CONSTRAINT'), verbose) self._prob = np.ones( num_scenarios) / num_scenarios if prob is None else np.asarray( prob) # result formatting options self._result: Optional[RegretResult] = None self._solution: Optional[RegretOptimizerSolution] = None assert isinstance( max_attempts, int) and max_attempts > 0, 'max_attempts must be an integer >= 1' self._max_attempts = max_attempts self._verbose = verbose
def constraint(w): return get_option("C.SCALE") * (d.volatility(w) - vol)
def constraint(w): return get_option("C.SCALE") * ( ret - d.expected_return(w, self.rebalance))
def objective(w): return (d.expected_return(w, self.rebalance) - self.penalties[i](w)) * get_option("F.SCALE")
def constraint(w): return get_option("C.SCALE") * ( cvar - d.cvar(w, self.rebalance, percentile))
def cvar(w): return get_option("F.SCALE") * (target - cube.cvar(w, True, 5.0))
def _ctr_max_cvar(w): return get_option("C.SCALE") * ( max_cvar - self.cvar_data.cvar(w, self.rebalance, percentile))
def obj_fun(w): return get_option("F.SCALE") * cube.expected_return(w, True)
def _ctr_max_vol(w): return get_option("C.SCALE") * (self.data.volatility(w) - max_vol)
def objective(w): return (self.data.expected_return(w, self.rebalance) - self.penalty(w)) * get_option("F.SCALE")