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
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.