Optimization#

The optimization module implements a set of methods intended for portfolio optimization. They follow the same API as scikit-learn’s estimator: the fit method takes X as the assets returns and stores the portfolio weights in its weights_ attribute.

X can be any array-like structure (numpy array, pandas DataFrame, etc.)

Naive Allocation#

The naive module implements a set of naive allocations commonly used as benchmarks for comparing different models:

Example:

Naive inverse-volatility allocation:

from sklearn.model_selection import train_test_split

from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import InverseVolatility
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)

model = InverseVolatility()
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)

Mean-Risk Optimization#

The MeanRisk estimator can solve the below 4 objective functions:

  • Minimize Risk:

\[\begin{split}\begin{cases} \begin{aligned} &\min_{w} & & risk_{i}(w) \\ &\text{s.t.} & & w^T\mu \ge min\_return \\ & & & A w \ge b \\ & & & risk_{j}(w) \le max\_risk_{j} \quad \forall \; j \ne i \end{aligned} \end{cases}\end{split}\]
  • Maximize Expected Return:

\[\begin{split}\begin{cases} \begin{aligned} &\max_{w} & & w^T\mu \\ &\text{s.t.} & & risk_{i}(w) \le max\_risk_{i} \\ & & & A w \ge b \\ & & & risk_{j}(w) \le max\_risk_{j} \quad \forall \; j \ne i \end{aligned} \end{cases}\end{split}\]
  • Maximize Utility:

\[\begin{split}\begin{cases} \begin{aligned} &\max_{w} & & w^T\mu - \lambda \times risk_{i}(w)\\ &\text{s.t.} & & risk_{i}(w) \le max\_risk_{i} \\ & & & w^T\mu \ge min\_return \\ & & & A w \ge b \\ & & & risk_{j}(w) \le max\_risk_{j} \quad \forall \; j \ne i \end{aligned} \end{cases}\end{split}\]
  • Maximize Ratio:

\[\begin{split}\begin{cases} \begin{aligned} &\max_{w} & & \frac{w^T\mu - r_{f}}{risk_{i}(w)}\\ &\text{s.t.} & & risk_{i}(w) \le max\_risk_{i} \\ & & & w^T\mu \ge min\_return \\ & & & A w \ge b \\ & & & risk_{j}(w) \le max\_risk_{j} \quad \forall \; j \ne i \end{aligned} \end{cases}\end{split}\]

With \(risk_{i}\) a risk measure among:

  • Variance

  • Semi-Variance

  • Standard-Deviation

  • Semi-Deviation

  • Mean Absolute Deviation

  • First Lower Partial Moment

  • CVaR (Conditional Value at Risk)

  • EVaR (Entropic Value at Risk)

  • Worst Realization (worst return)

  • CDaR (Conditional Drawdown at Risk)

  • Maximum Drawdown

  • Average Drawdown

  • EDaR (Entropic Drawdown at Risk)

  • Ulcer Index

  • Gini Mean Difference

It supports the following parameters:

  • Weight Constraints

  • Budget Constraints

  • Group Constrains

  • Transaction Costs

  • Management Fees

  • L1 and L2 Regularization

  • Turnover Constraint

  • Tracking Error Constraint

  • Uncertainty Set on Expected Returns

  • Uncertainty Set on Covariance

  • Expected Return Constraints

  • Risk Measure Constraints

  • Custom Objective

  • Custom Constraints

  • Prior Estimator

Example:

Maximum Sharpe Ratio portfolio:

from sklearn.model_selection import train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import 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)

model = MeanRisk(
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    risk_measure=RiskMeasure.VARIANCE,
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.sharpe_ratio)

Prior Estimator#

Every portfolio optimization has a parameter named prior_estimator. The prior estimator fits a PriorModel containing the estimation of assets expected returns, covariance matrix, returns and Cholesky decomposition of the covariance. It represents the investor’s prior beliefs about the model used to estimate such distribution.

The available prior estimators are:

Example:

Minimum Variance portfolio using a Factor Model:

from sklearn.model_selection import train_test_split

from skfolio.datasets import load_factors_dataset, load_sp500_dataset
from skfolio.optimization import MeanRisk
from skfolio.preprocessing import prices_to_returns
from skfolio.prior import FactorModel

prices = load_sp500_dataset()
factor_prices = load_factors_dataset()

X, y = prices_to_returns(prices, factor_prices)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, shuffle=False)

model = MeanRisk(prior_estimator=FactorModel())
model.fit(X_train, y_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)

Combining Prior Estimators#

Prior estimators can be combined together, making it possible to design complex models:

Example:

This example is purposely complex to demonstrate how multiple estimators can be combined.

The model below is a Maximum Sharpe Ratio optimization using a Factor Model for the estimation of the assets expected reruns and covariance matrix. A Black & Litterman model is used for the estimation of the factors expected reruns and covariance matrix, incorporating the analyst’ views on the factors. Finally, the Black & Litterman prior expected returns are estimated using an equal-weighted market equilibrium with a risk aversion of 2 and a denoised prior covariance matrix:

from sklearn.model_selection import train_test_split

from skfolio.datasets import load_factors_dataset, load_sp500_dataset
from skfolio.moments import DenoiseCovariance, EquilibriumMu
from skfolio.optimization import MeanRisk, ObjectiveFunction
from skfolio.preprocessing import prices_to_returns
from skfolio.prior import BlackLitterman, EmpiricalPrior, FactorModel

prices = load_sp500_dataset()
factor_prices = load_factors_dataset()

X, y = prices_to_returns(prices, factor_prices)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, shuffle=False)

factor_views = ["MTUM - QUAL == 0.0003 ",
                "SIZE - USMV == 0.0004",
                "VLUE == 0.0006"]

model = MeanRisk(
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    prior_estimator=FactorModel(
        factor_prior_estimator=BlackLitterman(
            prior_estimator=EmpiricalPrior(
                mu_estimator=EquilibriumMu(risk_aversion=2),
                covariance_estimator=DenoiseCovariance()
            ),
            views=factor_views)
    )
)

model.fit(X_train, y_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)

Custom Estimator#

It is very common to use a custom implementation for the moments estimators. For example, you may want to use an in-house estimation for the covariance or a predictive model for the expected returns.

Below is a simple example of how you would implement a custom covariance estimator. For more complex cases and estimators, check the API Reference.

import numpy as np

from skfolio.datasets import load_sp500_dataset
from skfolio.moments import BaseCovariance
from skfolio.optimization import MeanRisk
from skfolio.preprocessing import prices_to_returns
from skfolio.prior import EmpiricalPrior

prices = load_sp500_dataset()
X = prices_to_returns(prices)


class MyCustomCovariance(BaseCovariance):
    def __init__(self, my_param=0):
        super().__init__()
        self.my_param = my_param

    def fit(self, X, y=None):
        X = self._validate_data(X)
        # Your custom implementation goes here
        covariance = np.cov(X.T, ddof=self.my_param)
        self._set_covariance(covariance)
        return self


model = MeanRisk(
    prior_estimator=EmpiricalPrior(covariance_estimator=MyCustomCovariance(my_param=1)),
)
model.fit(X)

Worst-Case Optimization#

With the mu_uncertainty_set_estimator parameter, the expected returns of the assets are modeled with an ellipsoidal uncertainty set. This approach is known as worst-case optimization and falls under the class of robust optimization. It mitigates the instability that arises from estimation errors of the expected returns.

Example:

Worst-case maximum Mean/CDaR ratio (Conditional Drawdown at Risk) with an ellipsoidal uncertainty set for the expected returns of the assets:

from sklearn.model_selection import train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import MeanRisk, ObjectiveFunction
from skfolio.preprocessing import prices_to_returns
from skfolio.uncertainty_set import BootstrapMuUncertaintySet

prices = load_sp500_dataset()

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

model = MeanRisk(
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    risk_measure=RiskMeasure.CDAR,
    mu_uncertainty_set_estimator=BootstrapMuUncertaintySet(confidence_level=0.9),
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)
print(portfolio.cdar_ratio)

Going Further#

You can explore the remaining parameters (constraints, L1 and L2 regularization, costs, turnover, tracking error, etc.) with the Mean-Risk examples and the MeanRisk API.

Risk Budgeting#

The RiskBudgeting solves the below convex problem:

\[\begin{split}\begin{cases} \begin{aligned} &\min_{w} & & risk_{i}(w) \\ &\text{s.t.} & & b^T log(w) \ge c \\ & & & w^T\mu \ge min\_return \\ & & & A w \ge b \\ & & & w \ge0 \end{aligned} \end{cases}\end{split}\]

with \(b\) the risk budget vector and \(c\) an auxiliary variable of the log barrier.

And \(risk_{i}\) a risk measure among:

  • Variance

  • Semi-Variance

  • Standard-Deviation

  • Semi-Deviation

  • Mean Absolute Deviation

  • First Lower Partial Moment

  • CVaR (Conditional Value at Risk)

  • EVaR (Entropic Value at Risk)

  • Worst Realization (worst return)

  • CDaR (Conditional Drawdown at Risk)

  • Maximum Drawdown

  • Average Drawdown

  • EDaR (Entropic Drawdown at Risk)

  • Ulcer Index

  • Gini Mean Difference

  • First Lower Partial Moment

It supports the following parameters:

  • Weight Constraints

  • Budget Constraints

  • Group Constrains

  • Transaction Costs

  • Management Fees

  • Expected Return Constraints

  • Custom Objective

  • Custom constraints

  • Prior Estimator

Limitations are imposed on certain constraints, such as long-only weights, to ensure the problem remains convex.

Example:

CVaR (Conditional Value at Risk) Risk Parity portfolio:

from sklearn.model_selection import train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import RiskBudgeting
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)

model = RiskBudgeting(risk_measure=RiskMeasure.CVAR)
model.fit(X_train)
print(model.weights_)

portfolio_train = model.predict(X_train)
print(portfolio_train.annualized_sharpe_ratio)
print(portfolio_train.contribution(measure=RiskMeasure.CVAR))

portfolio_test = model.predict(X_test)
print(portfolio_test.annualized_sharpe_ratio)
print(portfolio_test.contribution(measure=RiskMeasure.CVAR))

Maximum Diversification#

The MaximumDiversification maximizes the diversification ratio, which is the ratio of the weighted volatilities over the total volatility.

Example:

from sklearn.model_selection import train_test_split

from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import MaximumDiversification
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)

model = MaximumDiversification()
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.diversification)

Distributionally Robust CVaR#

The DistributionallyRobustCVaR constructs a Wasserstein ball in the space of multivariate and non-discrete probability distributions centered at the uniform distribution on the training samples and finds the allocation that minimizes the CVaR of the worst-case distribution within this Wasserstein ball. Esfahani and Kuhn proved that for piecewise linear objective functions, which is the case of CVaR, the distributionally robust optimization problem over a Wasserstein ball can be reformulated as finite convex programs.

A solver like Mosek that can handle a high number of constraints is preferred.

Example:

from sklearn.model_selection import train_test_split

from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import DistributionallyRobustCVaR
from skfolio.preprocessing import prices_to_returns

prices = load_sp500_dataset()

X = prices_to_returns(prices)
X = X["2020":]
X_train, X_test = train_test_split(X, test_size=0.33, shuffle=False)

model = DistributionallyRobustCVaR(wasserstein_ball_radius=0.01)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.cvar)

Hierarchical Risk Parity#

The HierarchicalRiskParity (HRP) is a portfolio optimization method developed by Marcos Lopez de Prado.

This algorithm uses a distance matrix to compute hierarchical clusters using the Hierarchical Tree Clustering algorithm then employs seriation to rearrange the assets in the dendrogram, minimizing the distance between leafs.

The final step is the recursive bisection where each cluster is split between two sub-clusters by starting with the topmost cluster and traversing in a top-down manner. For each sub-cluster, we compute the total cluster risk of an inverse-risk allocation. A weighting factor is then computed from these two sub-cluster risks, which is used to update the cluster weight.

Note

The original paper uses the variance as the risk measure and the single-linkage method for the Hierarchical Tree Clustering algorithm. Here we generalize it to multiple risk measures and linkage methods. The default linkage method is set to the Ward variance minimization algorithm, which is more stable and has better properties than the single-linkage method.

It supports all prior estimators and risk measures as well as weight constraints.

It also supports all distance estimators through the distance_estimator parameter. It fits a distance model for the estimation of the codependence and the distance matrix used to compute the linkage matrix:

Example:

Hierarchical Risk Parity with semi (downside) standard-deviation as the risk measure and mutual information as the distance estimator:

from sklearn.model_selection import train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.distance import MutualInformation
from skfolio.optimization import HierarchicalRiskParity
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)

model = HierarchicalRiskParity(
    risk_measure=RiskMeasure.SEMI_DEVIATION, distance_estimator=MutualInformation()
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)
print(portfolio.contribution(measure=RiskMeasure.SEMI_DEVIATION))

Hierarchical Equal Risk Contribution#

The HierarchicalEqualRiskContribution (HERC) is a portfolio optimization method developed by Thomas Raffinot.

This algorithm uses a distance matrix to compute hierarchical clusters using the Hierarchical Tree Clustering algorithm. It then computes, for each cluster, the total cluster risk of an inverse-risk allocation.

The final step is the top-down recursive division of the dendrogram, where the assets weights are updated using a naive risk parity within clusters.

It differs from the Hierarchical Risk Parity by exploiting the dendrogram shape during the top-down recursive division instead of bisecting it.

Note

The default linkage method is set to the Ward variance minimization algorithm, which is more stable and has better properties than the single-linkage method.

It supports all prior estimators and risk measures as well as weight constraints.

It also supports all distance estimator through the distance_estimator parameter. It fits a distance model for the estimation of the codependence and the distance matrix used to compute the linkage matrix:

Example:

Hierarchical Equal Risk Contribution with CVaR (Conditional Value at Risk) as the risk measure and mutual information as the distance estimator:

from sklearn.model_selection import train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.distance import MutualInformation
from skfolio.optimization import HierarchicalEqualRiskContribution
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)

model = HierarchicalEqualRiskContribution(
    risk_measure=RiskMeasure.CVAR,
    distance_estimator = MutualInformation()
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)
print(portfolio.contribution(measure=RiskMeasure.CVAR))

Nested Clusters Optimization#

The NestedClustersOptimization (NCO) is a portfolio optimization method developed by Marcos Lopez de Prado.

It uses a distance matrix to compute clusters using a clustering algorithm ( Hierarchical Tree Clustering, KMeans, etc..). For each cluster, the inner-cluster weights are computed by fitting the inner-estimator on each cluster using the whole training data. Then the outer-cluster weights are computed by training the outer-estimator using out-of-sample estimates of the inner-estimators with cross-validation. Finally, the final assets weights are the dot-product of the inner-weights and outer-weights.

Note

The original paper uses KMeans as the clustering algorithm, minimum Variance for the inner-estimator and equal-weighted for the outer-estimator. Here we generalize it to all sklearn and skfolio clustering algorithms (Hierarchical Tree Clustering, KMeans, etc.), all portfolio optimizations (Mean-Variance, HRP, etc.) and risk measures (variance, CVaR, etc.). To avoid data leakage at the outer-estimator, we use out-of-sample estimates to fit the outer estimator.

It supports all distance estimator and clustering estimator (both skfolio and sklearn)

Example:

Nested Clusters Optimization with KMeans as the clustering algorithm, Kendall Distance as the distance estimator, Minimum Semi-Variance as the inner estimator, and CVaR Risk Parity as the outer (meta) estimator trained on the out-of-sample estimates from the KFolds cross-validation and run with parallelization:

from sklearn.cluster import KMeans
from sklearn.model_selection import KFold, train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.distance import KendallDistance
from skfolio.optimization import MeanRisk, NestedClustersOptimization, RiskBudgeting
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)

model = NestedClustersOptimization(
    inner_estimator=MeanRisk(risk_measure=RiskMeasure.SEMI_VARIANCE),
    outer_estimator=RiskBudgeting(risk_measure=RiskMeasure.CVAR),
    distance_estimator=KendallDistance(),
    clustering_estimator=KMeans(n_init="auto"),
    cv=KFold(),
    n_jobs=-1,
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)
print(portfolio.contribution(measure=RiskMeasure.CVAR))

The cv parameter can also be a combinatorial cross-validation, such as CombinatorialPurgedCV, in which case each cluster’s out-of-sample outputs are a collection of multiple paths instead of one single path. The selected out-of-sample path among this collection of paths is chosen according to the quantile and quantile_measure parameters.

Stacking Optimization#

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

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

Stacking allows to use 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.

Example:

Stacking Optimization with Minimum Semi-Variance and CVaR Risk Parity stacked together using Minimum Variance as the final (meta) estimator.

from sklearn.model_selection import KFold, train_test_split

from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import MeanRisk, RiskBudgeting, StackingOptimization
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)

estimators = [
    ('model1', MeanRisk(risk_measure=RiskMeasure.SEMI_VARIANCE)),
    ('model2', RiskBudgeting(risk_measure=RiskMeasure.CVAR))
]

model = StackingOptimization(
    estimators=estimators,
    final_estimator=MeanRisk(),
    cv=KFold(),
    n_jobs=-1
)
model.fit(X_train)
print(model.weights_)

portfolio = model.predict(X_test)
print(portfolio.annualized_sharpe_ratio)

The cv parameter can also be a combinatorial cross-validation, such as CombinatorialPurgedCV, in which case each out-of-sample outputs are a collection of multiple paths instead of one single path. The selected out-of-sample path among this collection of paths is chosen according to the quantile and quantile_measure parameters.