Note

Go to the end to download the full example code. or to run this example in your browser via Binder

# 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)