<a id="sphx-glr-auto-examples-ensemble-plot-1-stacking-py"></a>

<a id="stacking-optimization"></a>

# Stacking Optimization

This tutorial introduces the [`StackingOptimization`](https://skfolio.org/generated/skfolio.optimization.StackingOptimization.html.md#skfolio.optimization.StackingOptimization).

Stacking Optimization is an ensemble method that consists of stacking the output of
individual portfolio optimizations with a final portfolio optimization.

The weights are the dot-product of individual optimization weights with the final
optimization weights.

Stacking uses the strength of each individual portfolio optimization by using
their output as input of a final portfolio optimization.

To avoid data leakage, out-of-sample estimates are used to fit the outer optimization.

#### NOTE
The `estimators_` are fitted on the full `X` while `final_estimator_` is trained
using cross-validated predictions of the base estimators using `cross_val_predict`.

<a id="data"></a>

## Data

We load the FTSE 100 dataset. This dataset is composed of the daily prices of 64
assets from the FTSE 100 Index composition starting from 2000-01-04 up to 2023-05-31:

```Python
from plotly.io import show
from sklearn.model_selection import GridSearchCV, train_test_split

from skfolio import Population, RatioMeasure, RiskMeasure
from skfolio.datasets import load_ftse100_dataset
from skfolio.metrics import make_scorer
from skfolio.model_selection import (
    CombinatorialPurgedCV,
    WalkForward,
    cross_val_predict,
    optimal_folds_number,
)
from skfolio.moments import EmpiricalCovariance, LedoitWolf
from skfolio.optimization import (
    EqualWeighted,
    HierarchicalEqualRiskContribution,
    InverseVolatility,
    MaximumDiversification,
    MeanRisk,
    ObjectiveFunction,
    StackingOptimization,
)
from skfolio.preprocessing import prices_to_returns
from skfolio.prior import EmpiricalPrior

prices = load_ftse100_dataset()

X = prices_to_returns(prices)
X_train, X_test = train_test_split(X, test_size=0.50, shuffle=False)
```

<a id="stacking-model"></a>

## Stacking Model

Our stacking model will be composed of 4 models:
: * Inverse Volatility
  * Maximum Diversification
  * Maximum Mean-Risk Utility allowing short position with L1 regularization
  * Hierarchical Equal Risk Contribution

We will stack these 4 models together using the Mean-CDaR utility maximization:

```Python
estimators = [
    ("model1", InverseVolatility()),
    ("model2", MaximumDiversification(prior_estimator=EmpiricalPrior())),
    (
        "model3",
        MeanRisk(objective_function=ObjectiveFunction.MAXIMIZE_UTILITY, min_weights=-1),
    ),
    ("model4", HierarchicalEqualRiskContribution()),
]

model_stacking = StackingOptimization(
    estimators=estimators,
    final_estimator=MeanRisk(
        objective_function=ObjectiveFunction.MAXIMIZE_UTILITY,
        risk_measure=RiskMeasure.CDAR,
    ),
)
```

<a id="benchmark"></a>

## Benchmark

To compare the staking model, we use an equal-weighted benchmark:

```Python
benchmark = EqualWeighted()
```

<a id="parameter-tuning"></a>

## Parameter Tuning

To demonstrate how parameter tuning works in a staking model, we find the model
parameters that maximizes the out-of-sample Calmar Ratio using `GridSearchCV` with
`WalkForward` cross-validation on the training set.
The `WalkForward` splits are chosen to simulate a three-month (60 business days) rolling
portfolio fitted on the previous year (252 business days):

```Python
cv = WalkForward(train_size=252, test_size=60)

grid_search = GridSearchCV(
    estimator=model_stacking,
    cv=cv,
    n_jobs=-1,
    param_grid={
        "model2__prior_estimator__covariance_estimator": [
            EmpiricalCovariance(),
            LedoitWolf(),
        ],
        "model3__l1_coef": [0.001, 0.1],
        "model4__risk_measure": [
            RiskMeasure.VARIANCE,
            RiskMeasure.GINI_MEAN_DIFFERENCE,
        ],
    },
    scoring=make_scorer(RatioMeasure.CALMAR_RATIO),
)
grid_search.fit(X_train)
model_stacking = grid_search.best_estimator_
print(model_stacking)
```

```none
StackingOptimization(estimators=[('model1', InverseVolatility()),
                                 ('model2',
                                  MaximumDiversification(prior_estimator=EmpiricalPrior(covariance_estimator=EmpiricalCovariance()))),
                                 ('model3',
                                  MeanRisk(l1_coef=0.001, min_weights=-1,
                                           objective_function=MAXIMIZE_UTILITY)),
                                 ('model4',
                                  HierarchicalEqualRiskContribution())],
                     final_estimator=MeanRisk(objective_function=MAXIMIZE_UTILITY,
                                              risk_measure=CDaR))
```

<a id="prediction"></a>

## Prediction

We evaluate the Stacking model and the Benchmark using the same `WalkForward` object
on the test set:

```Python
pred_bench = cross_val_predict(
    benchmark,
    X_test,
    cv=cv,
    portfolio_params=dict(name="Benchmark"),
)

pred_stacking = cross_val_predict(
    model_stacking,
    X_test,
    cv=cv,
    n_jobs=-1,
    portfolio_params=dict(name="Stacking"),
)
```

Each predicted object is a `MultiPeriodPortfolio`.
For improved analysis, we can add them to a `Population`:

```Python
population = Population([pred_bench, pred_stacking])
```

Let’s plot the rolling portfolios cumulative returns on the test set:

```Python
population.plot_cumulative_returns()
```

[plotly figure stripped from llms output]
<br />
<br />

Let’s plot the rolling portfolios compositions:

```Python
population.plot_composition(display_sub_ptf_name=False)
```

[plotly figure stripped from llms output]
<br />
<br />

<a id="analysis"></a>

## Analysis

The Stacking model outperforms the Benchmark on the test set for the below ratios:

```Python
for ptf in population:
    print("=" * 25)
    print(" " * 8 + ptf.name)
    print("=" * 25)
    print(f"Sharpe ratio : {ptf.annualized_sharpe_ratio:0.2f}")
    print(f"CVaR ratio : {ptf.cdar_ratio:0.5f}")
    print(f"Calmar ratio : {ptf.calmar_ratio:0.5f}")
    print("\n")
```

```none
=========================
        Benchmark
=========================
Sharpe ratio : 0.79
CVaR ratio : 0.00263
Calmar ratio : 0.00122

=========================
        Stacking
=========================
Sharpe ratio : 0.82
CVaR ratio : 0.00305
Calmar ratio : 0.00125
```

Let’s display the full summary:

```Python
population.summary()
```

[plotly figure stripped from llms output]
<br />
<br />

```Python
print(
    "Average of Sharpe Ratio :"
    f" {pred_stacking.measures_mean(measure=RatioMeasure.ANNUALIZED_SHARPE_RATIO):0.2f}"
)
print(
    "Std of Sharpe Ratio :"
    f" {pred_stacking.measures_std(measure=RatioMeasure.ANNUALIZED_SHARPE_RATIO):0.2f}"
)
```

```none
Average of Sharpe Ratio : 0.84
Std of Sharpe Ratio : 0.10
```

Now, let’s analyze how the sub-models would have performed independently and compare
their distribution with the Stacking model:

```Python
population = Population([])
for model_name, model in model_stacking.estimators:
    pred = cross_val_predict(
        model,
        X_test,
        cv=cv,
        n_jobs=-1,
        portfolio_params=dict(tag=model_name),
    )
    population.extend(pred)
population.extend(pred_stacking)

fig = population.plot_distribution(
    measure_list=[RatioMeasure.ANNUALIZED_SHARPE_RATIO],
    n_bins=40,
    tag_list=["Stacking", "model1", "model2", "model3", "model4"],
)
show(fig)
```

<!doctype html>
[plotly figure stripped from llms output]

<a id="conclusion"></a>

## Conclusion

The Stacking model outperforms the Benchmark on the historical test set. The
distribution analysis on the recombined (non-historical) test sets shows that the
Stacking model continues to outperform the Benchmark in average.

**Total running time of the script:** (2 minutes 32.700 seconds)

<a id="sphx-glr-download-auto-examples-ensemble-plot-1-stacking-py"></a>
