Maximum Sharpe Ratio#

This tutorial uses the MeanRisk optimization to find the maximum Sharpe Ratio 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 InverseVolatility, 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 Maximum Sharpe Ratio 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 maximum Sharpe Ration portfolio:

model = MeanRisk(
    risk_measure=RiskMeasure.STANDARD_DEVIATION,
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    portfolio_params=dict(name="Max Sharpe"),
)
model.fit(X_train)
model.weights_
array([9.43840487e-02, 2.34371950e-08, 7.98221516e-09, 1.20893768e-01,
       3.18425289e-02, 1.42034916e-08, 1.69401899e-04, 1.24118110e-01,
       1.58750775e-08, 2.77981646e-02, 2.44878272e-08, 2.78735867e-08,
       1.16363413e-01, 5.73885972e-02, 1.96119090e-07, 1.09506078e-01,
       8.64772142e-02, 1.84019362e-01, 1.34686982e-02, 3.35703047e-02])

To compare this model, we use an inverse volatility benchmark using the InverseVolatility estimator:

benchmark = InverseVolatility(portfolio_params=dict(name="Inverse Vol"))
benchmark.fit(X_train)
benchmark.weights_
array([0.03306735, 0.02548697, 0.03551377, 0.0296872 , 0.06358463,
       0.05434705, 0.04742354, 0.07049715, 0.03882539, 0.06697905,
       0.05570808, 0.05576851, 0.04723274, 0.06351213, 0.05581397,
       0.0676481 , 0.02564642, 0.03970752, 0.05744543, 0.06610498])

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.00805141,  0.01084095,  0.00199143, ...,  0.00932287,
        0.00152752, -0.01787273])

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.annualized_sharpe_ratio)
print(pred_bench.annualized_sharpe_ratio)
1.0399693503172203
1.0036976120249752

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()
Max Sharpe Inverse Vol
Mean 0.073% 0.064%
Annualized Mean 18.43% 16.06%
Variance 0.012% 0.010%
Annualized Variance 3.14% 2.56%
Semi-Variance 0.0063% 0.0053%
Annualized Semi-Variance 1.58% 1.33%
Standard Deviation 1.12% 1.01%
Annualized Standard Deviation 17.72% 16.00%
Semi-Deviation 0.79% 0.73%
Annualized Semi-Deviation 12.58% 11.54%
Mean Absolute Deviation 0.74% 0.65%
CVaR at 95% 2.54% 2.35%
EVaR at 95% 5.11% 5.29%
Worst Realization 10.06% 10.49%
CDaR at 95% 13.81% 12.22%
MAX Drawdown 33.01% 34.83%
Average Drawdown 3.00% 2.34%
EDaR at 95% 19.53% 20.05%
First Lower Partial Moment 0.37% 0.32%
Ulcer Index 0.048 0.040
Gini Mean Difference 1.11% 0.98%
Value at Risk at 95% 1.54% 1.45%
Drawdown at Risk at 95% 10.23% 8.79%
Entropic Risk Measure at 95% 3.00 3.00
Fourth Central Moment 0.000029% 0.000022%
Fourth Lower Partial Moment 0.000011% 0.000011%
Skew 29.58% -14.94%
Kurtosis 1888.73% 2117.02%
Sharpe Ratio 0.066 0.063
Annualized Sharpe Ratio 1.04 1.00
Sortino Ratio 0.092 0.088
Annualized Sortino Ratio 1.47 1.39
Mean Absolute Deviation Ratio 0.099 0.098
First Lower Partial Moment Ratio 0.20 0.20
Value at Risk Ratio at 95% 0.047 0.044
CVaR Ratio at 95% 0.029 0.027
Entropic Risk Measure Ratio at 95% 0.00024 0.00021
EVaR Ratio at 95% 0.014 0.012
Worst Realization Ratio 0.0073 0.0061
Drawdown at Risk Ratio at 95% 0.0071 0.0073
CDaR Ratio at 95% 0.0053 0.0052
Calmar Ratio 0.0022 0.0018
Average Drawdown Ratio 0.024 0.027
EDaR Ratio at 95% 0.0037 0.0032
Ulcer Index Ratio 0.015 0.016
Gini Mean Difference Ratio 0.066 0.065
Effective Number of Assets 8.913374763803748 18.460872007821077
Assets Number 20 20


Conclusion#

From the analysis on the test set, we see that the Maximum Sharpe Ratio portfolio outperform the inverse-volatility benchmark for the mean and the ratio measures including the Sharpe Ratio, and underperforms for the deviation and shortfall 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 2.182 seconds)

Gallery generated by Sphinx-Gallery