Minimum CVaR#

This tutorial uses the MeanRisk optimization to find the minimum CVaR (Conditional Value at Risk) portfolio.

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. Prices are transformed into linear returns (see data preparation) and split into a training set and a test set without shuffling to avoid data leakage.

import numpy as np
from plotly.io import show
from sklearn.model_selection import train_test_split

from skfolio import Population, RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import EqualWeighted, MeanRisk, ObjectiveFunction
from skfolio.preprocessing import prices_to_returns

prices = load_sp500_dataset()

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

print(X_train.head())
                AAPL       AMD       BAC  ...       UNH       WMT       XOM
Date                                      ...
1990-01-03  0.007576 -0.030303  0.008045  ... -0.019355  0.000000 -0.010079
1990-01-04  0.003759 -0.015500 -0.021355  ... -0.009868 -0.005201 -0.009933
1990-01-05  0.003745 -0.031996 -0.021821  ... -0.043189 -0.010732 -0.005267
1990-01-08  0.003731  0.000000  0.005633  ... -0.020833  0.013630  0.015381
1990-01-09 -0.007435  0.016527  0.000000  ... -0.024823 -0.026619 -0.020114

[5 rows x 20 columns]

Model#

We create a Minimum CVaR model and then fit it on the training set. portfolio_params are parameters passed to the Portfolio returned by the predict method. It can be omitted, here we use it to give a name to our minimum CVaR portfolio:

model = MeanRisk(
    risk_measure=RiskMeasure.CVAR,
    objective_function=ObjectiveFunction.MINIMIZE_RISK,
    portfolio_params=dict(name="Min CVaR"),
)
model.fit(X_train)
model.weights_
array([2.16562529e-02, 1.41742527e-10, 3.64888981e-11, 1.47358386e-02,
       1.35927671e-01, 1.91035768e-10, 3.58350612e-10, 2.10319630e-01,
       4.89793803e-11, 8.14733606e-02, 1.92817471e-02, 4.13607836e-10,
       7.79844555e-10, 1.26155697e-01, 4.04585759e-10, 1.52708811e-01,
       1.20106233e-02, 6.41726679e-03, 1.01024013e-01, 1.18289085e-01])

To compare this model, we use an equal-weighted benchmark using EqualWeighted:

benchmark = EqualWeighted(portfolio_params=dict(name="Equal Weighted"))
# Even if `X` has no impact (as it is equal weighted), we still need to call `fit` for
# API consistency.
benchmark.fit(X_train)
benchmark.weights_
array([0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05])

Prediction#

We predict the model and the benchmark on the test set:

pred_model = model.predict(X_test)
pred_bench = benchmark.predict(X_test)

The predict method returns a Portfolio object.

Portfolio is an array-container making it compatible with scikit-learn tools: calling np.asarray(pred_model) gives the portfolio returns (same as pred_model.returns):

np.asarray(pred_model)
array([ 0.00354605,  0.00342423, -0.00105085, ...,  0.01069377,
        0.00542224, -0.01217676])

The Portfolio class contains a vast number of properties and methods used for analysis.

For example:
  • pred_model.plot_cumulative_returns()

  • pred_model.plot_composition()

  • pred_model.summary()

print(pred_model.cvar)
print(pred_bench.cvar)
0.021741551637176403
0.025061083134673378

Analysis#

For improved analysis, we load both predicted portfolios into a Population:

population = Population([pred_model, pred_bench])

The Population class also contains a vast number of properties and methods used for analysis. Let’s plot each portfolio composition:

population.plot_composition()


Note

Every plot methods in skfolio returns a plotly figure. To display a plotly figure, you may need to call show() and change the default renderer: https://plotly.com/python/renderers/

Let’s plot each portfolio cumulative returns:

fig = population.plot_cumulative_returns()
# show(fig) is only used for the documentation sticker.
show(fig)

Finally, let’s display the full summary of both strategies evaluated on the test set:

population.summary()
Min CVaR Equal Weighted
Mean 0.052% 0.069%
Annualized Mean 13.07% 17.30%
Variance 0.0090% 0.012%
Annualized Variance 2.26% 2.94%
Semi-Variance 0.0046% 0.0060%
Annualized Semi-Variance 1.16% 1.52%
Standard Deviation 0.95% 1.08%
Annualized Standard Deviation 15.02% 17.15%
Semi-Deviation 0.68% 0.78%
Annualized Semi-Deviation 10.76% 12.32%
Mean Absolute Deviation 0.61% 0.71%
CVaR at 95% 2.17% 2.51%
EVaR at 95% 4.62% 5.43%
Worst Realization 8.53% 10.77%
CDaR at 95% 14.81% 13.35%
MAX Drawdown 34.70% 34.70%
Average Drawdown 3.12% 2.67%
EDaR at 95% 20.48% 20.49%
First Lower Partial Moment 0.31% 0.35%
Ulcer Index 0.050 0.044
Gini Mean Difference 0.92% 1.06%
Value at Risk at 95% 1.33% 1.54%
Drawdown at Risk at 95% 11.23% 9.48%
Entropic Risk Measure at 95% 3.00 3.00
Fourth Central Moment 0.000016% 0.000027%
Fourth Lower Partial Moment 0.000008% 0.000013%
Skew -0.13% -2.79%
Kurtosis 1976.27% 1959.90%
Sharpe Ratio 0.055 0.064
Annualized Sharpe Ratio 0.87 1.01
Sortino Ratio 0.076 0.088
Annualized Sortino Ratio 1.21 1.40
Mean Absolute Deviation Ratio 0.085 0.097
First Lower Partial Moment Ratio 0.17 0.19
Value at Risk Ratio at 95% 0.039 0.045
CVaR Ratio at 95% 0.024 0.027
Entropic Risk Measure Ratio at 95% 0.00017 0.00023
EVaR Ratio at 95% 0.011 0.013
Worst Realization Ratio 0.0061 0.0064
Drawdown at Risk Ratio at 95% 0.0046 0.0072
CDaR Ratio at 95% 0.0035 0.0051
Calmar Ratio 0.0015 0.0020
Average Drawdown Ratio 0.017 0.026
EDaR Ratio at 95% 0.0025 0.0033
Ulcer Index Ratio 0.010 0.015
Gini Mean Difference Ratio 0.056 0.064
Effective Number of Assets 7.461275191312059 19.999999999999993
Assets Number 20 20


Conclusion#

From the analysis on the test set, we see that the Minimum CVaR portfolio outperforms the equal-weighted benchmark for all deviation and shortfall risk measures, except for the drawdown measures, and underperforms for the mean and ratio measures.

See also

This was a toy example, for more advanced concepts check the user guide or the other examples.

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

Gallery generated by Sphinx-Gallery