Constraints

Here we define many realistic constraints that apply to portfolio optimization trading policies.

Some of them, like LongOnly, are very simple to use. Some others are more advanced, for example FactorNeutral takes time-varying factor exposures as parameters.

For a minimal example we present the classic Markowitz allocation.

import cvxportfolio as cvx

objective = cvx.ReturnsForecast() - gamma_risk * cvx.FullCovariance()

# the policy takes a list of constraint instances
constraints = [cvx.LongOnly(applies_to_cash=True)]

policy = cvx.SinglePeriodOptimization(objective, constraints)
print(cvx.MarketSimulator(universe).backtest(policy))

With this, we require that the optimal post-trade weights found by the single-period optimization policy are non-negative. In our formulation the full portfolio weights vector (which includes the cash account) sums to one, see equation 4.9 at page 43 of the paper.

class cvxportfolio.constraints.DollarNeutralView on GitHub

Long-short dollar neutral strategy.

In our notation, this is

\[\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 \({(w_t + z_t)}_{n+1} = 1\).

class cvxportfolio.constraints.MarketNeutral(benchmark=<class 'cvxportfolio.policies.MarketBenchmark'>, **kwargs)View on GitHub

Simple implementation of β- (or market-) neutrality.

In our notation, this is

\[{(w_t^\text{b})}^T \Sigma_t (w_t + z_t) = 0\]

The benchmark portfolio weights are given by a Policy object chosen by the user.

Added in version 1.2.0: This constraint’s interface has been improved: now you can pass any policy object as benchmark, and give parameters to the forecaster of \(\Sigma_t\).

Parameters:
  • benchmark (cvx.Policy class or instance) – Policy object whose target weights at each point in time are the benchmark weights we neutralize against. You can pass a class or an instance. If you pass a class it is instantiated with default parameters. Default is cvxportfolio.MarketBenchmark, which are weights proportional to the previous year’s total traded volumes.

  • kwargs (dict) – Optional arguments passed to the initializer of cvxportfolio.forecast.HistoricalFactorizedCovariance, like rolling window or exponential smoothing half life, for the estimation of the covariance matrices \(\Sigma_t\). Default (no other arguments) is to use its default parameters.

class cvxportfolio.constraints.FactorMaxLimit(factor_exposure, limit)View on GitHub

A max limit on portfolio-wide factor (e.g. beta) exposure.

It models the term:

\[f_t^T {(w_t^+)}_{1:n} \leq l_t\]

where \(f_t\) is the factor exposure vector and \(l_t\) the limit at time \(t\). It can also model a vector constraint

\[F_t^T {(w_t^+)}_{1:n} \leq l_t\]

where \(F_t\) is a matrix of factor exposures and \(l_t\) a vector of limits at time \(t\). See below for details.

Parameters:
  • factor_exposure (pd.Series or pd.DataFrame) – 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 and has the assets names as columns, and it represents factor exposures that change in time. In the latter case an observation must be present for every point in time of a backtest. If you want you can also pass multiple factor exposures at once: as a dataframe indexed by assets’ names and whose columns are the factors (if constant in time), or a dataframe with multiindex: first level is time, second level are assets’ names (if changing in time). However this latter usecase is probably better served by making multiple instances of this constraint, one for each factor.

  • limit (float or pd.Series or pd.DataFrame) – Factor limit, either constant or varying in time. Use a DataFrame if you pass multiple factors as once.

class cvxportfolio.constraints.FactorMinLimit(factor_exposure, limit)View on GitHub

A min limit on portfolio-wide factor (e.g. beta) exposure.

It models the term:

\[f_t^T {(w_t^+)}_{1:n} \geq l_t\]

where \(f_t\) is the factor exposure vector and \(l_t\) the limit at time \(t\). It can also model a vector constraint

\[F_t^T {(w_t^+)}_{1:n} \geq l_t\]

where \(F_t\) is a matrix of factor exposures and \(l_t\) a vector of limits at time \(t\). See below for details.

Parameters:
  • factor_exposure (pd.Series or pd.DataFrame) – 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 and has the assets names as columns, and it represents factor exposures that change in time. In the latter case an observation must be present for every point in time of a backtest. If you want you can also pass multiple factor exposures at once: as a dataframe indexed by assets’ names and whose columns are the factors (if constant in time), or a dataframe with multiindex: first level is time, second level are assets’ names (if changing in time). However this latter usecase is probably better served by making multiple instances of this constraint, one for each factor.

  • limit (float or pd.Series or pd.DataFrame) – Factor limit, either constant or varying in time. Use a DataFrame if you pass multiple factors as once.

class cvxportfolio.constraints.FactorGrossLimit(factor_exposure, limit)View on GitHub

A gross limit on portfolio-wide factor (e.g. beta) exposure.

It models the term:

\[f_t^T |{(w_t^+)}_{1:n}| \leq l_t\]

where \(f_t\) is the factor exposure vector and \(l_t\) the limit at time \(t\). It can also model a vector constraint

\[F_t^T |{(w_t^+)}_{1:n}| \leq l_t\]

where \(F_t\) is a matrix of factor exposures and \(l_t\) a vector of limits at time \(t\). See below for details.

Parameters:
  • factor_exposure (pd.Series or pd.DataFrame) – Series or DataFrame giving the factor exposure. All elements must be non-negative. If Series it is indexed by assets’ names and represents factor exposures constant in time. If DataFrame it is indexed by time and has the assets names as columns, and it represents factor exposures that change in time. In the latter case an observation must be present for every point in time of a backtest. If you want you can also pass multiple factor exposures at once: as a dataframe indexed by assets’ names and whose columns are the factors (if constant in time), or a dataframe with multiindex: first level is time, second level are assets’ names (if changing in time). However this latter usecase is probably better served by making multiple instances of this constraint, one for each factor.

  • limit (float or pd.Series or pd.DataFrame) – Factor limit, either constant or varying in time. Use a DataFrame if you pass multiple factors as once.

class cvxportfolio.constraints.FactorNeutral(factor_exposure)View on GitHub

Require neutrality with respect to certain risk factors.

This is developed at page 35 of the paper. This models the term

\[{(f_t)}^T {(w_t^+)}_{1:n} = 0\]

where \(f_t\) is the factor exposure vector. It can also model a vector constraint

\[F_t^T {(w_t^+)}_{1:n} = 0\]

where \(F_t\) is a matrix of factor exposures. See below for details.

Parameters:

factor_exposure (pd.Series or pd.DataFrame) – 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 and has the assets names as columns, and it represents factor exposures that change in time. In the latter case an observation must be present for every point in time of a backtest. If you want you can also pass multiple factor exposures at once: as a dataframe indexed by assets’ names and whose columns are the factors (if constant in time), or a dataframe with multiindex: first level is time, second level are assets’ names (if changing in time). However this latter usecase is probably better served by making multiple instances of this constraint, one for each factor.

class cvxportfolio.constraints.FixedFactorLoading(factor_exposure, target)View on GitHub

A constraint to fix portfolio loadings to a set of factors.

This can be used to impose market neutrality, a certain portfolio-wide alpha, ….

It models the term:

\[f_t^T {(w_t^+)}_{1:n} = l_t\]

where \(f_t\) is the factor exposure vector and \(l_t\) the limit at time \(t\). It can also model a vector constraint

\[F_t^T {(w_t^+)}_{1:n} = l_t\]

where \(F_t\) is a matrix of factor exposures and \(l_t\) a vector of limits at time \(t\). See below for details.

Parameters:
  • factor_exposure (pd.Series or pd.DataFrame) – 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 and has the assets names as columns, and it represents factor exposures that change in time. In the latter case an observation must be present for every point in time of a backtest. If you want you can also pass multiple factor exposures at once: as a dataframe indexed by assets’ names and whose columns are the factors (if constant in time), or a dataframe with multiindex: first level is time, second level are assets’ names (if changing in time). However this latter usecase is probably better served by making multiple instances of this constraint, one for each factor.

  • target (float or pd.Series or pd.DataFrame) – Target portfolio factor exposures, either constant or varying in time. Use a DataFrame if you pass multiple factors as once.

class cvxportfolio.constraints.LeverageLimit(limit)View on GitHub

Constraints on the leverage of the portfolio.

In the notation of the book, this is

\[\|{(w_t + z_t)}_{1:n}\|_1 \leq L^\text{max},\]

where \((w_t + z_t)\) are the post-trade weights, and we exclude the cash account from the \(\ell_1\) norm.

Parameters:

limit (float or pd.Series) – Constant or varying in time leverage limit \(L^\text{max}\). If varying in time it is expressed as a pd.Series with datetime index.

class cvxportfolio.constraints.LongCashView on GitHub

Require that post-trade cash account is non-negative.

In our notation, this is

\[{(w_t + z_t)}_{n+1} \geq 0.\]

Be mindful that trading costs (if present) are deducted from the cash account after trading, and so the next period’s cash account value may be negative.

class cvxportfolio.constraints.LongOnly(applies_to_cash=False)View on GitHub

A long only constraint.

\[w_t + z_t \geq 0\]

Imposes that at each point in time the post-trade 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).

Parameters:

applies_to_cash (bool) – Whether the long only requirement also applies to the cash account.

class cvxportfolio.constraints.MaxWeights(limit)View on GitHub

A max limit on post-trade weights (excluding cash).

In our notation, this is

\[{(w_t + z_t)}_{1:n} \leq w^\text{max}\]

where the limit \(w^\text{max}\) is either a scalar or a vector, see below.

Parameters:

limit (float, pandas.Series, pandas.DataFrame) – A series or number giving the weights limit. See the 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.

class cvxportfolio.constraints.MinWeights(limit)View on GitHub

A min limit on post-trade weights (excluding cash).

In our notation, this is

\[{(w_t + z_t)}_{1:n} \geq w^\text{min}\]

where the limit \(w^\text{min}\) is either a scalar or a vector, see below.

Parameters:

limit (float, pandas.Series, pandas.DataFrame) – A series or number giving the weights limit. See the 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.

class cvxportfolio.constraints.MaxBenchmarkDeviation(limit)View on GitHub

A max limit on post-trade weights minus the benchmark weights.

In our notation, this is

\[{(w_t + z_t - w^\text{bm}_t)}_{1:n} \leq w^\text{max}\]

where the limit \(w^\text{max}\) is either a scalar or a vector, see below.

Added in version 1.1.0: Added in version 1.1.0

Parameters:

limit (float, pandas.Series, pandas.DataFrame) – A series or number giving the weights limit. See the 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.

class cvxportfolio.constraints.MinBenchmarkDeviation(limit)View on GitHub

A min limit on post-trade weights minus the benchmark weights.

In our notation, this is

\[{(w_t + z_t - w^\text{bm}_t)}_{1:n} \geq w^\text{min}\]

where the limit \(w^\text{min}\) is either a scalar or a vector, see below.

Added in version 1.1.0: Added in version 1.1.0

Parameters:

limit (float, pandas.Series, pandas.DataFrame) – A series or number giving the weights limit. See the 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.

class cvxportfolio.constraints.ParticipationRateLimit(volumes, max_fraction_of_volumes=0.05)View on GitHub

A limit on maximum trades size as a fraction of market volumes.

Parameters:
  • volumes (pd.Series or pd.DataFrame) – per-stock and per-day market volume estimates, or constant in time

  • max_fraction_of_volumes (float, pd.Series, pd.DataFrame) – max fraction of market volumes that we’re allowed to trade

class cvxportfolio.constraints.TurnoverLimit(delta)View on GitHub

Turnover limit as a fraction of the portfolio value.

The turnover is defined as half the \(\ell_1\)-norm of the trade weight vector, without cash. Here we ask that it is smaller than some constant:

\[\|{(z_t)}_{1:n}\|_1/2 \leq \delta.\]
Parameters:

delta (float or pd.Series) – We require that the turnover over each trading period is smaller than this value. This is either constant, expressed as float, or changing in time, expressed as a pd.Series with datetime index.

Soft constraints

(Almost) all the constraints described above can also be be made “soft”. The concept is developed in section 4.6 of the paper, and implemented by the objective term cvxportfolio.SoftConstraint.

In the case of a linear equality constraint, which can be expressed as \(h(x) = 0\), the corresponding soft constraint is a cost of the form \(\gamma \|h(x)\|_1\), where \(\gamma\) is the priority penalizer.

For a linear inequality constraint \(h(x) \leq 0\), instead, the corresponding soft constraint is the cost \(\gamma \|{(h(x))}_+\|_1\), where \({(\cdot )}_+\) denotes the positive part of each element of \(h(x)\).

In the paper we describe having different penalizers for different elements of each constraint vector (so that the penalizer \(\gamma\) is a vector). In our implementation this is achieved by constructing multiple soft constraints, each with a scalar \(\gamma\) penalizer.

The syntax of our implementation is very simple. We pass a constraint instance to cvxportfolio.SoftConstraint, multiply the term by the penalizer, and subtract it from the objective function. For a high value of the penalizer the constraint will be enforced almost exactly. For a small value, it will be almost ignored.

For example:

policy = cvx.SinglePeriodOptimization(
    objective =
        cvx.ReturnsForecast()
        - 0.5 * cvx.FullCovariance()
        - 10 * cvx.SoftConstraint(cvx.LeverageLimit(3)))

is a policy that almost enforces a leverage limit of 3, allowing for some violation. This can be controlled by tuning the multiplier in front of cvxportfolio.SoftConstraint.

Some constraint objects, which are in reality compositions of constraints, can not be used as soft constraints. See their documentation for more details.

Cost inequality as constraint

Since version 0.4.6 you can use any term described in the objective terms page as part of an inequality constraint. In fact, you can use any linear combination of objective terms. For a minimal example see the following risk-constrained policy.

import cvxportfolio as cvx

# limit the covariance
risk_limit = cvx.FullCovariance() <= target_volatility**2

cvx.MarketSimulator(universe).backtest(
    cvx.SinglePeriodOptimization(cvx.ReturnsForecast(), [risk_limit])).plot()

Keep in mind that the resulting inequality constraint must be convex. You can’t, for example, require that a risk term is larger or equal than some value.

Base classes (for extending Cvxportfolio)

class cvxportfolio.constraints.ConstraintView on GitHub

Base cvxpy constraint class.

class cvxportfolio.constraints.EqualityConstraintView on GitHub

Base class for equality constraints.

This class is not exposed to the user, each equality constraint inherits from this and overrides the InequalityConstraint._compile_constr_to_cvxpy() and InequalityConstraint._rhs() methods.

We factor this code in order to streamline the design of SoftConstraint costs.

class cvxportfolio.constraints.InequalityConstraintView on GitHub

Base class for inequality constraints.

This class is not exposed to the user, each inequality constraint inherits from this and overrides the InequalityConstraint._compile_constr_to_cvxpy() and InequalityConstraint._rhs() methods.

We factor this code in order to streamline the design of SoftConstraint costs.