Skip to content

Commit

Permalink
Added constraint on min/max benchmark deviation
Browse files Browse the repository at this point in the history
  • Loading branch information
enzbus committed Dec 29, 2023
1 parent 757eca3 commit adad9c9
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 0 deletions.
73 changes: 73 additions & 0 deletions cvxportfolio/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"ParticipationRateLimit",
"MaxWeights",
"MinWeights",
"MaxBenchmarkDeviation",
"MinBenchmarkDeviation",
"NoTrade",
"NoCash",
"FactorMaxLimit",
Expand Down Expand Up @@ -617,6 +619,77 @@ def _rhs(self):
"""Compile right hand side of the constraint expression."""
return -self.limit.parameter

class MaxBenchmarkDeviation(MaxWeights):
r"""A max limit on post-trade weights minus the benchmark weights.
In our notation, this is
.. math::
{(w_t + z_t - w^\text{bm}_t)}_{1:n} \leq w^\text{max}
where the limit :math:`w^\text{max}` is either a scalar or a vector, see
below.
.. versionadded:: 1.1.0
Added in version 1.1.0
:param limit: A series or number giving the weights limit. See the
:ref:`passing-data` manual page for details on how to provide this
data. For example, you pass a float if you want a constant limit
for all assets at all times, a Pandas series indexed by time if you
want a limit constant for all assets but varying in time, a Pandas
series indexed by the assets' names if you have limits constant in time
but different for each asset, and a Pandas dataframe indexed by time
and with assets as columns if you have a different limit for each point
in time and each asset. If the value changes for each asset, you should
provide a value for each name that ever appear in a back-test; the
data will be sliced according to the current trading universe during a
back-test. It is fine to have missing values at certain times on assets
that are not traded then.
:type limit: float, pandas.Series, pandas.DataFrame
"""

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_minus_w_bm[:-1]


class MinBenchmarkDeviation(MinWeights):
r"""A min limit on post-trade weights minus the benchmark weights.
In our notation, this is
.. math::
{(w_t + z_t - w^\text{bm}_t)}_{1:n} \geq w^\text{min}
where the limit :math:`w^\text{min}` is either a scalar or a vector, see
below.
.. versionadded:: 1.1.0
Added in version 1.1.0
:param limit: A series or number giving the weights limit. See the
:ref:`passing-data` manual page for details on how to provide this
data. For example, you pass a float if you want a constant limit
for all assets at all times, a Pandas series indexed by time if you
want a limit constant for all assets but varying in time, a Pandas
series indexed by the assets' names if you have limits constant in time
but different for each asset, and a Pandas dataframe indexed by time
and with assets as columns if you have a different limit for each point
in time and each asset. If the value changes for each asset, you should
provide a value for each name that ever appear in a back-test; the
data will be sliced according to the current trading universe during a
back-test. It is fine to have missing values at certain times on assets
that are not traded then.
:type limit: float, pandas.Series, pandas.DataFrame
"""

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_minus_w_bm[:-1]


class MinMaxWeightsAtTimes(Estimator):
"""This class abstracts functionalities used by the two below.
Expand Down
67 changes: 67 additions & 0 deletions cvxportfolio/tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,73 @@ def test_min_weights(self):
model.values_in_time_recursive(t=self.returns.index[2])
self.assertFalse(cons.value())

def test_max_bm_dev(self):
"""Test max benchmark deviation constraint."""
model = cvx.MaxBenchmarkDeviation(2)
cons = self._build_constraint(model)
self.w_plus_minus_w_bm.value = np.ones(self.N) / self.N
self.assertTrue(cons.value())
tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertFalse(cons.value())

model = cvx.MaxBenchmarkDeviation(7)
cons = self._build_constraint(model)

tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertTrue(cons.value())

limits = pd.Series(index=self.returns.index, data=2)
limits.iloc[1] = 7

model = cvx.MaxBenchmarkDeviation(limits)
cons = self._build_constraint(model, t=self.returns.index[1])

tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertTrue(cons.value())
model.values_in_time_recursive(t=self.returns.index[2])
self.assertFalse(cons.value())

def test_min_bm_dev(self):
"""Test min benchmark deviation constraint."""
model = cvx.MinBenchmarkDeviation(2)
cons = self._build_constraint(model, self.returns.index[1])

self.w_plus_minus_w_bm.value = np.ones(self.N) / self.N
self.assertFalse(cons.value())
tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertFalse(cons.value())
model = cvx.MinBenchmarkDeviation(-3)
cons = self._build_constraint(model, self.returns.index[1])
tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertTrue(cons.value())

limits = pd.Series(index=self.returns.index, data=2)
limits.iloc[1] = -3
model = cvx.MinBenchmarkDeviation(limits)
cons = self._build_constraint(model, t=self.returns.index[1])
tmp = np.zeros(self.N)
tmp[0] = 4
tmp[-1] = -3
self.w_plus_minus_w_bm.value = tmp
self.assertTrue(cons.value())
model.values_in_time_recursive(t=self.returns.index[2])
self.assertFalse(cons.value())

def test_factor_max_limit(self):
"""Test factor max limit constraint."""

Expand Down
4 changes: 4 additions & 0 deletions docs/constraints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Constraints

.. autoclass:: MinWeights

.. autoclass:: MaxBenchmarkDeviation

.. autoclass:: MinBenchmarkDeviation

.. autoclass:: ParticipationRateLimit

.. autoclass:: TurnoverLimit
Expand Down

0 comments on commit adad9c9

Please sign in to comment.