Weight Constraints#

This tutorial shows how to incorporate weight constraints into the MeanRisk optimization.

We will show how to use the below parameters:
  • min_weights

  • max_weights

  • budget

  • min_budget

  • max_budget

  • max_short

  • max_long

  • linear_constraints

  • groups

  • left_inequality

  • right_inequality

Data#

We load the S&P 500 dataset composed of the daily prices of 20 assets from the S&P 500 Index composition starting from 1990-01-02 up to 2022-12-28. We select only 3 assets to make the example more readable, which are Apple (AAPL), General Electric (GE) and JPMorgan (JPM):

import numpy as np
from plotly.io import show

from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import MeanRisk
from skfolio.preprocessing import prices_to_returns

prices = load_sp500_dataset()
prices = prices[["AAPL", "GE", "JPM"]]

X = prices_to_returns(prices)

Model#

In this tutorial, we will use a Minimum Variance model. By default, MeanRisk is long only (min_weights=0) and fully invested (budget=1). In other terms, all weights are positive and sum to one.

model = MeanRisk()
model.fit(X)
print(sum(model.weights_))
model.weights_
1.0

array([0.22768876, 0.56566507, 0.20664617])

Budget#

The budget is the sum of long positions and short positions (sum of all weights). It can be None or a float. None means that there are no budget constraints. The default is 1.0 (fully invested).

Examples:

  • budget = 1 –> fully invested portfolio

  • budget = 0 –> market neutral portfolio

  • budget = None –> no constraints on the sum of weights

model = MeanRisk(budget=0.5)
model.fit(X)
print(sum(model.weights_))
model.weights_
0.5

array([0.11391513, 0.28246101, 0.10362386])

You can also set a constraint on the minimum and maximum budget using min_budget and max_budget, which are the lower and upper bounds of the sum of long and short positions (sum of all weights). The default is None. If provided, you must set budget=None.

model = MeanRisk(budget=None, min_budget=0.3, max_budget=0.5)
model.fit(X)
print(sum(model.weights_))
model.weights_
0.30000034617916516

array([0.06832987, 0.16956647, 0.06210401])

Lower and Upper Bounds on Weights#

The weights lower and upper bounds are controlled by the parameters min_weights and max_weights respectively. You can provide None, a float, an array-like or a dictionary. None is equivalent to -np.Inf (no lower bounds). If a float is provided, it is applied to each asset. If a dictionary is provided, its (key/value) pair must be the (asset name/asset weight bound) and the input X of the fit method must be a DataFrame with the assets names in columns. The default values are min_weights=0.0 (no short selling) and max_weights=1.0 (each asset is below 100%). When using a dictionary, you don’t have to provide constraints for all assets. If not provided, the default values (0.0 for min_weights and 1.0 for max_weights) will be assigned to the assets not specified in the dictionary.

Note

When incorporating a pre-selection transformer into a Pipeline, using a list for weight constraints is not feasible, as we don’t know in advance which assets will be selected by the pre-selection process. This is where the dictionary proves useful.

Example:
  • min_weights = 0 –> long only portfolio (no short selling).

  • min_weights = None –> no lower bound (same as -np.Inf).

  • min_weights = -2 –> each weight must be above -200%.

  • min_weights = [0, -2, 0.5] –> “AAPL”, “GE” and “JPM” must be above 0%, -200% and 50% respectively.

  • min_weights = {“AAPL”: 0, “GE”: -2} -> “AAPL”, “GE” and “JPM” must be above 0%, -200% and 0% (default) respectively.

  • max_weights = 0 –> no long position (short only portfolio).

  • max_weights = None –> no upper bound (same as +np.Inf).

  • max_weights = 2 –> each weight must be below 200%.

  • max_weights = [1, 2, -0.5] -> “AAPL”, “GE” and “JPM” must be below 100%, 200% and -50% respectively.

  • max_weights = {“AAPL”: 1, “GE”: 2} -> “AAPL”, “GE” and “JPM” must be below 100%, 200% and 100% (default).

Let’s create a model that allows short positions with a budget of -100%:

model = MeanRisk(budget=-1, min_weights=-1)
model.fit(X)
print(sum(model.weights_))
model.weights_
-1.0000000000000002

array([-0.22770271, -0.56559255, -0.20670474])

Let’s add weight constraints on “AAPL”, “GE” and “JPM” to be above 0%, 50% and 10% respectively:

model = MeanRisk(min_weights=[0, 0.5, 0.1])
model.fit(X)
print(sum(model.weights_))
model.weights_
1.0000000000000002

array([0.22788246, 0.56548525, 0.20663228])

Let’s plot the composition:

portfolio = model.predict(X)
fig = portfolio.plot_composition()
show(fig)

Let’s create the same model as above but using partial dictionary:

model = MeanRisk(min_weights={"GE": 0.5, "JPM": 0.1})
model.fit(X)
print(sum(model.weights_))
model.weights_
1.0000000000000002

array([0.22788246, 0.56548525, 0.20663228])

Let’s create a model with a leverage of 3 and every weights below 150%:

model = MeanRisk(budget=3, max_weights=1.5)
model.fit(X)
print(sum(model.weights_))
model.weights_
3.000000000000002

array([0.74197781, 1.49999867, 0.75802352])

Short and Long Position Constraints#

Constraints on the upper bound for short and long positions can be set using max_short and max_long. The short position is defined as the sum of negative weights (in absolute term) and the long position as the sum of positive weights.

Let’s create a fully invested long-short portfolio model with a total short position less than 50%:

model = MeanRisk(min_weights=-1, max_short=0.5)
model.fit(X)
print(sum(model.weights_))
model.weights_
1.0

array([0.22770146, 0.56558315, 0.20671539])

Group and Linear Constraints#

We can assign groups to each asset using the groups parameter and set constraints on these groups using the linear_constraint parameter. The groups parameter can be a 2D array-like or a dictionary. If a dictionary is provided, its (key/value) pair must be the (asset name/asset groups). You can reference these groups and/or the asset names in linear_constraint, which is a list if strings following the below patterns:

  • “2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3”

  • “ref1 >= 2.9 * ref2”

  • “ref1 <= ref2”

  • “ref1 >= ref1”

Let’s create a model with groups constraints on “industry sector” and “capitalization”:

groups = {
    "AAPL": ["Technology", "Mega Cap"],
    "GE": ["Industrial", "Big Cap"],
    "JPM": ["Financial", "Big Cap"],
}
# You can also provide a 2D array-like:
# groups = [["Technology", "Industrial", "Financial"], ["Mega Cap", "Big Cap", "Big Cap"]]
linear_constraints = [
    "Technology + 1.5 * Industrial <= 2 * Financial",  # First group
    "Mega Cap >= 0.75 * Big Cap",  # Second group
    "Technology >= Big Cap",  # Mix of first and second groups
    "Mega Cap >= 2 * JPM",  # Mix of groups and assets
]
# Note that only the first constraint would be sufficient in that case.

model = MeanRisk(groups=groups, linear_constraints=linear_constraints)
model.fit(X)
model.weights_
array([6.66666667e-01, 1.17341817e-11, 3.33333333e-01])

Left and Right Inequalities#

Finally, you can also directly provide the matrix \(A\) and the vector \(b\) of the linear constraint \(A \cdot w \leq b\):

left_inequality = np.array(
    [[1.0, 1.5, -2.0], [-1.0, 0.75, 0.75], [-1.0, 1.0, 1.0], [-1.0, -0.0, 2.0]]
)
right_inequality = np.array([0.0, 0.0, 0.0, 0.0])

model = MeanRisk(left_inequality=left_inequality, right_inequality=right_inequality)
model.fit(X)
model.weights_
array([6.66666667e-01, 1.17341817e-11, 3.33333333e-01])

Total running time of the script: (0 minutes 0.318 seconds)

Gallery generated by Sphinx-Gallery