Black & Litterman Factor Model#

This tutorial shows how to use the FactorModel estimator coupled with the BlackLitterman estimator in the MeanRisk optimization.

The Black & Litterman Factor Model is a Factor Model in which we incorporate views on factors using the Black & Litterman Model.

In the previous two tutorials, we introduced the Factor Model and the Black & Litterman separately. In this tutorial we show how we can merge them together by building a Maximum Sharpe Ratio portfolio using the FactorModel estimator.

Data#

We load the S&P 500 dataset composed of the daily prices of 20 assets from the SPX Index composition and the Factors dataset composed of the daily prices of 5 ETF representing common factors:

from plotly.io import show
from sklearn.model_selection import train_test_split

from skfolio import Population, RiskMeasure
from skfolio.datasets import load_factors_dataset, load_sp500_dataset
from skfolio.optimization import MeanRisk, ObjectiveFunction
from skfolio.preprocessing import prices_to_returns
from skfolio.prior import BlackLitterman, FactorModel

prices = load_sp500_dataset()
factor_prices = load_factors_dataset()

prices = prices["2014":]
factor_prices = factor_prices["2014":]

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)

Analyst views#

Let’s assume we are able to accurately estimate views about future realization of the factors. We estimate that the factor Size will have an expected return of 10% p.a. (absolute view) and will outperform the factor Value by 3% p.a. (relative view). We also estimate the factor Momentum will outperform the factor Quality by 2% p.a (relative view). By converting these annualized estimates into daily estimates to be homogenous with the input X, we get:

factor_views = [
    "SIZE == 0.00039",
    "SIZE - VLUE == 0.00011 ",
    "MTUM - QUAL == 0.00007",
]

Black & Litterman Factor Model#

We create a Maximum Sharpe Ratio model using the Black & Litterman Factor Model that we fit on the training set:

model_bl_factor = MeanRisk(
    risk_measure=RiskMeasure.VARIANCE,
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    prior_estimator=FactorModel(
        factor_prior_estimator=BlackLitterman(views=factor_views),
    ),
    portfolio_params=dict(name="Black & Litterman Factor Model"),
)
model_bl_factor.fit(X_train, y_train)
model_bl_factor.weights_
array([5.56000809e-02, 2.42685508e-02, 3.64005580e-02, 1.97994501e-02,
       8.93188830e-11, 1.69581067e-02, 1.34097308e-01, 1.19789402e-09,
       2.78013891e-02, 9.56214479e-02, 6.72469426e-02, 9.04183614e-02,
       1.24556425e-01, 8.98242729e-02, 5.85688593e-02, 2.83522508e-02,
       6.69632764e-03, 1.02313728e-01, 2.14759387e-02, 9.32493866e-11])

For comparison, we also create a Maximum Sharpe Ratio model using a simple Factor Model:

model_factor = MeanRisk(
    risk_measure=RiskMeasure.VARIANCE,
    objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
    prior_estimator=FactorModel(),
    portfolio_params=dict(name="Factor Model"),
)
model_factor.fit(X_train, y_train)
model_factor.weights_
array([1.22188511e-09, 1.31737525e-03, 4.74253021e-10, 7.70440099e-10,
       9.46436338e-10, 1.24959219e-09, 5.13001762e-02, 6.35652050e-02,
       7.36336705e-10, 1.79104637e-01, 5.03133290e-02, 7.13696569e-02,
       4.12895735e-02, 2.27967880e-01, 5.13346758e-02, 1.44128512e-01,
       2.88142338e-10, 6.19657594e-02, 5.63432134e-02, 1.09849875e-09])

Prediction#

We predict both models on the test set:

ptf_bl_factor_test = model_bl_factor.predict(X_test)
ptf_factor_test = model_factor.predict(X_test)

population = Population([ptf_bl_factor_test, ptf_factor_test])

population.plot_cumulative_returns()


Because our factor views were accurate, the Black & Litterman Factor Model outperformed the simple Factor Model on the test set.

Let’s plot the portfolios compositions:

fig = population.plot_composition()
show(fig)

Going Further#

The API design makes it possible to created nested models without limits. In the below example, we re-apply a Black & Litterman model incorporating assets views. But instead of using the empirical moments, we use the above Black & Litterman factor model:

assets_views = [
    "AAPL == 0.00098",
    "AAPL - GE == 0.00086",
    "JPM - GE == 0.00059",
]

model = BlackLitterman(
    views=assets_views,
    prior_estimator=FactorModel(
        factor_prior_estimator=BlackLitterman(views=factor_views),
    ),
)

model.fit(X, y)
print(model.prior_model_.covariance.shape)
(20, 20)

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

Gallery generated by Sphinx-Gallery