Skip to content

Commit

Permalink
minor doc edits
Browse files Browse the repository at this point in the history
  • Loading branch information
enzbus committed Sep 6, 2023
1 parent 14f243d commit abe3698
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 32 deletions.
118 changes: 86 additions & 32 deletions cvxportfolio/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
Here we define many realistic constraints that apply to
:ref:`portfolio optimization trading policies <optimization-policies-page>`.
Some of them, like :class:`LongOnly`, do not take any parameter, and are
very simple to use. Some others, for example :class:`FactorMaxLimit`, are more advanced
(that one takes time-varying factor exposures as parameters).
Some of them, like :class:`LongOnly`, are
very simple to use. Some others are more advanced,
for example :class:`FactorNeutral`
takes time-varying factor exposures as parameters.
For a minimal example we present the classic Markowitz allocation.
Expand Down Expand Up @@ -54,6 +55,7 @@
"LeverageLimit",
"LongCash",
"DollarNeutral",
"FactorNeutral",
"ParticipationRateLimit",
"MaxWeights",
"MinWeights",
Expand Down Expand Up @@ -274,14 +276,15 @@ def _rhs(self):
class LongOnly(BaseWeightConstraint, InequalityConstraint):
"""A long only constraint.
..math::
.. math::
w_t + z_t \geq 0
Imposes that at each point in time the post-trade
weights are non-negative. By default we only apply it
to the non-cash assets, but you can also require that
the cash account is non-negative.
weights are non-negative. By default it applies
to all elements of the post-trade weights vector
but you can also exclude the cash account (and let
cash be negative).
:param applies_to_cash: Whether the long only requirement
also applies to the cash account.
Expand Down Expand Up @@ -328,13 +331,20 @@ def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):


class LeverageLimit(BaseWeightConstraint, InequalityConstraint):
"""A limit on leverage.
Leverage is defined as the :math:`\ell_1` norm of non-cash
post-trade weights. Here we require that it is smaller than
a given value.
r"""Constraints on the leverage of the portfolio.
In the notation of the book, this is
.. math::
\|{(w_t + z_t)}_{1:n}\|_1 \leq L^\text{max},
where :math:`(w_t + z_t)` are the post-trade weights, and we
exclude the cash account from the :math:`\ell_1` norm.
:param limit: constant or varying in time leverage limit
:param limit: Constant or varying in time leverage limit
:math:`L^\text{max}`. If varying in time it is expressed
as a :class:`pd.Series` with datetime index.
:type limit: float or pd.Series
"""

Expand All @@ -350,12 +360,22 @@ def _rhs(self):
return self.limit.parameter


class MinCashBalance(BaseWeightConstraint):
"""Requires that the cash account is larger than c_min dollars.
class MinCashBalance(BaseWeightConstraint, InequalityConstraint):
"""Require that the cash balance is above a threshold.
In our notation this is
.. math::
{(w_t + z_t)}_{n+1} \geq c_\text{min} / v_t,
where :math:`v_t` is the portfolio value at time :math:`t`.
:param c_min: The miminimum cash balance required,
either constant in time or varying. This is expressed
in dollars.
:type c_min: float or pd.Series
This uses logic to subtract cash used as margin for the short
positions that is not documented in the book but is
equivalent to the book definition's for long-only stock positions.
"""

def __init__(self, c_min):
Expand All @@ -364,23 +384,35 @@ def __init__(self, c_min):

def _values_in_time(self, current_portfolio_value, **kwargs):
self.rhs.value = self.c_min.current_value/current_portfolio_value

def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
"""Return a Cvxpy constraint."""
# TODO clarify this
realcash = (w_plus[-1] - 2 * cp.sum(cp.neg(w_plus[:-1])))
return realcash >= self.rhs

def _compile_constr_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
"Compile left hand side of the constraint expression."
return -w_plus[-1]

def _rhs(self):
"Compile right hand side of the constraint expression."
return -self.rhs


class LongCash(MinCashBalance):
"""Requires that cash be non-negative."""
"""Require that cash be non-negative."""

def __init__(self):
super().__init__(0.)


class DollarNeutral(BaseWeightConstraint, EqualityConstraint):
"""Long-short dollar neutral strategy."""
"""Long-short dollar neutral strategy.
In our notation, this is
.. math::
\mathbf{1}^T \max({(w_t + z_t)}_{1:n}, 0) =
-\mathbf{1}^T \min({(w_t + z_t)}_{1:n}, 0)
which is simply :math:`{(w_t + z_t)}_{n+1} = 1`.
"""

def _compile_constr_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
"Compile left hand side of the constraint expression."
Expand All @@ -394,8 +426,8 @@ def _rhs(self):
class MaxWeights(BaseWeightConstraint, InequalityConstraint):
"""A max limit on weights.
Attributes:
limit: A series or number giving the weights limit.
:param limit: A series or number giving the weights limit.
:type limit: float or pd.Series
"""

def __init__(self, limit):
Expand All @@ -413,8 +445,8 @@ def _rhs(self):
class MinWeights(BaseWeightConstraint, InequalityConstraint):
"""A min limit on weights.
Attributes:
limit: A series or number giving the weights limit.
:param limit: A series or number giving the weights limit.
:type limit: float or pd.Series
"""

def __init__(self, limit):
Expand Down Expand Up @@ -476,8 +508,8 @@ def _rhs(self):


class FactorMaxLimit(BaseWeightConstraint, InequalityConstraint):
"""A max limit on portfolio-wide factor (e.g. beta) exposure.
r"""A max limit on portfolio-wide factor (e.g. beta) exposure.
:param factor_exposure: Series or DataFrame giving the factor exposure.
If Series it is indexed by assets' names and represents factor
exposures constant in time. If DataFrame it is indexed by time
Expand Down Expand Up @@ -587,3 +619,25 @@ def _compile_constr_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
def _rhs(self):
"Compile right hand side of the constraint expression."
return self.target.parameter

class FactorNeutral(FixedFactorLoading):
r"""Require neutrality with respect to certain risk factors.
This is developed at page 35 of
`the book <https://stanford.edu/~boyd/papers/pdf/cvx_portfolio.pdf>`_.
We require
.. math::
{(F_t)}^T_i (w_t + z_t) = 0,
where :math:`{(F_t)}_i` is the exposure to the :math:`i`-th factor
of a risk model at time :math:`t`.
:param factor_exposure: Either constant (if Series) or varying in time
(if Dataframe with datetime index) factor exposure.
:type factor_exposure: pd.Series or pd.DataFrame
"""

def __init__(self, factor_exposure):
super().__init(self, factor_exposure, 0.)
2 changes: 2 additions & 0 deletions docs/constraints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Constraints

.. autoclass:: FactorMinLimit

.. autoclass:: FactorNeutral

.. autoclass:: FixedFactorLoading

.. autoclass:: LeverageLimit
Expand Down

0 comments on commit abe3698

Please sign in to comment.