Skip to content

markowitz.optimizer.constraints

markowitz.optimizer.constraints

Pluggable portfolio constraints.

Each constraint is a frozen dataclass that knows how to materialise itself into a list of CVXPY constraints once it is told (a) which decision variable to wire itself to and (b) any auxiliary context (current ticker order, whether the problem has been reformulated, etc.).

The two-step __init__ / apply_cvxpy(w, ctx) split is deliberate: constraints are described declaratively by the caller, then re-applied freshly to a brand-new :class:cvxpy.Problem on every solve. This avoids the well-known "stale parameter" foot-guns that come with mutating a single long-lived problem.

Constraint

Bases: Protocol

Anything that knows how to attach itself to a CVXPY problem.

ConstraintContext(tickers: tuple[str, ...], n_assets: int, long_only: bool = False, extra: Mapping[str, Any] = dict()) dataclass

Read-only snapshot of optimizer state available to a constraint.

The context is constructed once per solve call and passed to every constraint's :meth:Constraint.apply_cvxpy method, so each constraint can use the canonical ticker ordering without making assumptions about the optimizer's internals.

CustomConstraint(builder: Callable[[cp.Variable, ConstraintContext], list[cp.Constraint]], description: str = 'custom') dataclass

Escape hatch for user-defined CVXPY constraints.

builder is a callable (w, ctx) -> list[cvxpy.Constraint]; it is invoked freshly each solve so it should not close over solver state.

Source code in src/markowitz/optimizer/constraints.py
def __init__(
    self,
    builder: Callable[[cp.Variable, ConstraintContext], list[cp.Constraint]],
    description: str = "custom",
) -> None:
    object.__setattr__(self, "builder", builder)
    object.__setattr__(self, "_description", description)

LeverageCap(max_leverage: float) dataclass

Gross-exposure cap sum(|w_i|) <= max_leverage.

LongOnly() dataclass

No short sales: w_i >= 0 for every asset.

SectorCap(sector_mapping: Mapping[str, str], caps: Mapping[str, float], floors: Mapping[str, float] | None = None) dataclass

Per-sector exposure caps (and optional floors).

sector_mapping maps each ticker to the name of its sector; caps (and optionally floors) map sector names to scalar weight bounds. Tickers not mentioned in the mapping are simply ignored from the grouping (treated as their own singleton "sector").

TurnoverCap(prev_weights: pd.Series | np.ndarray, max_turnover: float) dataclass

L1 turnover budget against a previous-period weight vector.

sum(|w - prev|) <= max_turnover.

WeightBounds(lower: float | Sequence[float], upper: float | Sequence[float]) dataclass

Element-wise box constraint lower <= w_i <= upper.

The bounds may be supplied as scalars (broadcast across all assets) or as iterables of length n_assets.