<a id="sphx-glr-auto-examples-mean-risk-plot-17-failure-and-fallbacks-py"></a>

<a id="failure-and-fallbacks"></a>

# Failure and Fallbacks

This tutorial introduces the optimization parameters `fallback` and `raise_on_failure`.

Optimization can sometimes fail during a given rebalancing. For example, a convex
mean-variance problem with strict risk or sector constraints may become infeasible on
specific dates. Such failures must be handled explicitly depending on the use case
(production vs. research).

<a id="fallback"></a>

## Fallback

The `fallback` parameter lets you define an estimator, or a list of estimators, to try
in order when the primary optimization raises an error during `fit`. Alternatively, you
can use `"previous_weights"` to reuse the last valid allocation.

Each attempt is recorded in `fallback_chain_`, and the successful estimator is available
through `fallback_`.

This mechanism is essential in automated pipelines, ensuring that optimization failures
never halt production runs while preserving full reproducibility and traceability.
Beyond safeguarding workflows, it can also be used to deliberately relax constraints in
a controlled manner when strict convergence cannot be achieved.

<a id="raise-on-failure"></a>

## Raise on Failure

In research, cross-validation and hyperparameter tuning (e.g. walk-forward, multiple
randomized cross-validation), it’s often useful to let all runs complete while keeping
a full record of failures instead of stopping on the first failed rebalancing.

- Set `raise_on_failure=True` (default) to fail fast. This is useful in production when
  the primary optimization or the fallback cascade is expected to succeed.
- Set `raise_on_failure=False` to continue uninterrupted. This is useful in research
  and cross-validation. When a failure occurs, `predict` returns a
  [`FailedPortfolio`](https://skfolio.org/generated/skfolio.portfolio.FailedPortfolio.html.md#skfolio.portfolio.FailedPortfolio) (think of it as an augmented NaN) that
  carries diagnostics such as `optimization_error` and `fallback_chain`, while remaining
  API-compatible with downstream analytics.

<a id="data-and-setup"></a>

## Data and Setup

Load the S&P 500 [dataset](https://skfolio.org/user_guide/datasets.html.md#datasets) and split into train/test.

```Python
import pandas as pd
from plotly.io import show
from sklearn.model_selection import train_test_split
from sklearn.utils.validation import validate_data

from skfolio.datasets import load_sp500_dataset
from skfolio.model_selection import WalkForward, cross_val_predict
from skfolio.optimization import BaseOptimization, EqualWeighted, MeanRisk
from skfolio.preprocessing import prices_to_returns
from skfolio.typing import Fallback, MultiInput
from skfolio.utils.stats import rand_weights

# Load S&P 500 dataset and split train/test
prices = load_sp500_dataset()
prices = prices["2010":]
X = prices_to_returns(prices)
X_train, X_test = train_test_split(X, test_size=0.33, shuffle=False)
```

<a id="id1"></a>

## Fallback

Let’s start with a simple example.
The primary model is a minimum-variance optimization made intentionally infeasible
(the assets’ minimum weights are set to 10%, which exceeds the feasible upper bound
of 1/n_assets = 5%). As a fallback, we provide a feasible minimum-variance model
with a 2% minimum weight constraint:

```Python
model = MeanRisk(
    min_weights=0.1,  # intentionally infeasible
    fallback=MeanRisk(min_weights=0.02),  # feasible fallback
)
model.fit(X_train)
print(model.weights_)
```

```none
[0.02000143 0.02000011 0.02000017 0.02000038 0.02000046 0.02000042
 0.02000085 0.12397314 0.02000024 0.09473377 0.02004185 0.02000133
 0.02000057 0.17901455 0.0200018  0.17643479 0.02000026 0.02000095
 0.12579215 0.02000078]
```

<a id="diagnostics"></a>

### Diagnostics

Let’s retrieve the fitted fallback that produced the final result:

```Python
print(model.fallback_)
```

```none
MeanRisk(min_weights=0.02)
```

Let’s display the sequence of attempts and their outcomes:

```Python
print(model.fallback_chain_)
```

```none
[('MeanRisk(fallback=MeanRisk(min_weights=0.02), min_weights=0.1)', "Solver 'CLARABEL' failed. Try another solver, or solve with solver_params=dict(verbose=True) for more information"), ('MeanRisk(min_weights=0.02)', 'success')]
```

The fallback audit trail is also propagated to the predicted portfolio:

```Python
portfolio = model.predict(X_test)
assert portfolio.fallback_chain == model.fallback_chain_
```

<a id="multiple-fallbacks"></a>

### Multiple fallbacks

We can also provide a list of fallbacks to be tried in order, including
“previous_weights” as a terminal safety net:

```Python
model = MeanRisk(
    min_weights=0.1,
    previous_weights={
        "AAPL": 0.4,
        "AMD": 0.2,
        "UNH": 0.4,
    },  # any missing assets default to 0
    fallback=[
        MeanRisk(min_weights=0.02),
        MeanRisk(min_weights=0.01),
        EqualWeighted(),
        "previous_weights",
    ],
)
```

<a id="chaining"></a>

### Chaining

We can also nest fallbacks.
The chain is evaluated depth-first from the primary estimator to the
first successful fallback, recording each attempt in `fallback_chain_`.
This is equivalent to providing an ordered list:

```Python
model = MeanRisk(
    min_weights=0.1,
    fallback=MeanRisk(
        min_weights=0.02,
        fallback=MeanRisk(
            min_weights=0.01,
            fallback=EqualWeighted(),
        ),
    ),
)
```

<a id="fallback-in-cross-validation"></a>

## Fallback in cross-validation

Fallback behavior is fully preserved in cross-validation.

When using `cross_val_predict`, all diagnostics (e.g., fallback chains and errors)
are propagated to the resulting portfolios in the `MultiPeriodPortfolio`:

- Each individual [`Portfolio`](https://skfolio.org/generated/skfolio.portfolio.Portfolio.html.md#skfolio.portfolio.Portfolio)
  (or [`FailedPortfolio`](https://skfolio.org/generated/skfolio.portfolio.FailedPortfolio.html.md#skfolio.portfolio.FailedPortfolio)) produced during rebalancing
  carries its own `fallback_chain` and `optimization_error`.
- Global counts and statistics (e.g., the number of portfolios that required a
  fallback) are available through summary attributes such as
  `n_fallback_portfolios` and `n_failed_portfolios`.
- The `summary()` method consolidates performance and diagnostic information
  across all rebalances.

```Python
model = MeanRisk(min_weights=0.1, fallback=MeanRisk(min_weights=0.02))

# Rebalance semiannually on the third Friday (WOM-3FRI), training on the prior 12 months
walk_forward = WalkForward(test_size=6, train_size=12, freq="WOM-3FRI")

pred = cross_val_predict(model, X, cv=walk_forward)
```

Let’s retrieve the fallback chain of the first portfolio:

```Python
print(pred[0].fallback_chain)
```

```none
[('MeanRisk(fallback=MeanRisk(min_weights=0.02), min_weights=0.1)', "Solver 'CLARABEL' failed. Try another solver, or solve with solver_params=dict(verbose=True) for more information"), ('MeanRisk(min_weights=0.02)', 'success')]
```

Let’s print the number of portfolios in
[`MultiPeriodPortfolio`](https://skfolio.org/generated/skfolio.portfolio.MultiPeriodPortfolio.html.md#skfolio.portfolio.MultiPeriodPortfolio) where a fallback was used:

```Python
print(pred.n_fallback_portfolios)
```

```none
23
```

Finally, let’s display the last four rows of the `MultiPeriodPortfolio` summary,
which contain the fallback statistics:

```Python
print(pred.summary().iloc[-4:])
```

```none
Avg nb of Assets per Portfolio    20.0
Number of Portfolios                23
Number of Failed Portfolios          0
Number of Fallback Portfolios       23
dtype: str
```

<a id="failure-handling"></a>

## Failure handling

In this section, we show how to handle optimization failures using the
`raise_on_failure` parameter.
As an example, we create a custom optimization that intentionally fails during `fit`
when the first date of the input window falls on an even day of the month, or when
`always_fail=True`.

```Python
class CustomOptimization(BaseOptimization):
    """Dummy optimization that intentionally fails during `fit` when the first
    date of the input window is an even day-of-month, or when `always_fail=True`."""

    def __init__(
        self,
        always_fail: bool = False,
        portfolio_params: dict | None = None,
        fallback: Fallback = None,
        previous_weights: MultiInput | None = None,
        raise_on_failure: bool = True,
    ):
        super().__init__(
            portfolio_params=portfolio_params,
            fallback=fallback,
            raise_on_failure=raise_on_failure,
            previous_weights=previous_weights,
        )
        self.always_fail = always_fail

    def fit(self, X: pd.DataFrame, y=None):
        validate_data(self, X)
        # Fail when first observation date has an even day-of-month, or always.
        if self.always_fail:
            raise RuntimeError("Forced failure")
        first_day = X.index[0].day
        if first_day % 2 == 0:
            raise RuntimeError("Forced failure (even-start window)")
        n_assets = X.shape[1]
        self.weights_ = rand_weights(n_assets)
        return self
```

By default, as with all scikit-learn estimators, failures raise an error during `fit`:

```Python
model = CustomOptimization(always_fail=True)
try:
    model.fit(X_train)
except RuntimeError as err:
    print(err)
```

```none
Forced failure
```

By setting `raise_on_failure=False`, a warning is emitted instead of raising an error,
and `weights_` are set to `None`, with the error message stored in `error_`:

```Python
model = CustomOptimization(always_fail=True, raise_on_failure=False)
model.fit(X_train)
print(model.weights_)
print(model.error_)
```

```none
None
Forced failure
```

In this case, calling `predict` will return a `FailedPortfolio` carrying the audit
trail in `optimization_error` and `fallback_chain` (if any fallbacks occurred).

```Python
portfolio = model.predict(X_test)
print(portfolio)
print(portfolio.optimization_error)
```

```none
<FailedPortfolio CustomOptimization>
Forced failure
```

Setting `raise_on_failure=False` is useful for cross-validation and hyperparameter
tuning as it allows all runs to complete without stopping at the first rebalancing
failure. Let’s instantiate our custom optimization and run a walk-forward analysis
where failures occur deterministically on even-start windows:

```Python
model = CustomOptimization(raise_on_failure=False)
pred = cross_val_predict(model, X, cv=walk_forward)
```

`cross_val_predict` completed without interruption.
The resulting `MultiPeriodPortfolio` is composed of both `Portfolio` and
`FailedPortfolio` objects:

```Python
print(pred.portfolios)
```

```none
[<Portfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <FailedPortfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>, <Portfolio CustomOptimization>]
```

Let’s print the number of failed portfolios:

```Python
print(pred.n_failed_portfolios)
```

```none
9
```

Even though `MultiPeriodPortfolio` contains failed portfolios, all statistics and
plots still work properly. This is because `FailedPortfolio` is designed to behave
like non-propagating NaNs:

```Python
print(pred.summary())
```

```none
Mean                                     0.061%
Annualized Mean                          15.42%
Variance                               0.000080
Annualized Variance                       2.01%
Semi-Variance                          0.000042
Annualized Semi-Variance                  1.06%
Standard Deviation                        0.89%
Annualized Standard Deviation            14.18%
Semi-Deviation                            0.65%
Annualized Semi-Deviation                10.28%
Mean Absolute Deviation                   0.65%
CVaR at 95%                               2.07%
EVaR at 95%                               2.76%
Worst Realization                         4.48%
CDaR at 95%                              13.47%
MAX Drawdown                             25.55%
Average Drawdown                          2.58%
EDaR at 95%                              16.76%
First Lower Partial Moment                0.33%
Ulcer Index                               0.042
Gini Mean Difference                      0.96%
Value at Risk at 95%                      1.45%
Drawdown at Risk at 95%                   9.43%
Entropic Risk Measure at 95%               3.00
Fourth Central Moment                 0.000004%
Fourth Lower Partial Moment           0.000002%
Skew                                    -20.14%
Kurtosis                                552.21%
Sharpe Ratio                              0.069
Annualized Sharpe Ratio                    1.09
Sortino Ratio                             0.094
Annualized Sortino Ratio                   1.50
Mean Absolute Deviation Ratio             0.094
First Lower Partial Moment Ratio           0.19
Value at Risk Ratio at 95%                0.042
CVaR Ratio at 95%                         0.030
Entropic Risk Measure Ratio at 95%      0.00020
EVaR Ratio at 95%                         0.022
Worst Realization Ratio                   0.014
Drawdown at Risk Ratio at 95%            0.0065
CDaR Ratio at 95%                        0.0045
Calmar Ratio                             0.0024
Average Drawdown Ratio                    0.024
EDaR Ratio at 95%                        0.0037
Ulcer Index Ratio                         0.014
Gini Mean Difference Ratio                0.064
Avg nb of Assets per Portfolio             20.0
Number of Portfolios                         23
Number of Failed Portfolios                   9
Number of Fallback Portfolios                 0
dtype: str
```

As shown below, `MultiPeriodPortfolio` plots gracefully handle `FailedPortfolio`
instances; for cumulative returns, these appear as gaps corresponding to failed
periods:

```Python
fig = pred.plot_cumulative_returns()
show(fig)
```

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

<br/>

Finally, let’s inspect the first failed portfolio:

```Python
failed_ptf = pred.failed_portfolios[0]
print(failed_ptf.optimization_error)
```

```none
Forced failure (even-start window)
```

To replay the optimization on the failed period, we can run:

```Python
# model.fit(failed_ptf.X)
```

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

<a id="sphx-glr-download-auto-examples-mean-risk-plot-17-failure-and-fallbacks-py"></a>
