What Is the Black-Litterman Model?

The Black-Litterman (BL) Model is an analytical tool used by portfolio managers to optimize asset allocation within an investor’s risk tolerance and market views.

The BL model starts from a neutral position using modern portfolio theory (MPT), and then takes additional input from investors’ views to determine how the ultimate asset allocation should deviate from the initial portfolio weights.

I will use the PyPortfolioOpt is a library that implements portfolio optimization methods, including classical efficient frontier techniques and Black-Litterman allocation.

We will also need yfinance to retrieve the quotes of my securities

After installing the library, we execute

import pandas as pd
import yfinance as yf
import openpyxl
from currency_converter import CurrencyConverter
import numpy as np
from pypfopt import BlackLittermanModel
from pypfopt import black_litterman, risk_models
# Declare constants

PORTFOLIO='/mnt/wd-bigfoot/share/luis/OneDrive/Documents/Portfolio/00 portfolio.xlsx'
ETFS_SHEET='ETFs'
START_DATE='2010-01-01'
END_DATE='2021-01-08'

ALL_WORLD='IWDA.AS'

TOTAL_FUNDS=10e3 # 10.000 EUR
# Open the portfolio and get the ETFs

etfs_pd = pd.read_excel(PORTFOLIO, engine='openpyxl', sheet_name=ETFS_SHEET)
etfs_pd.index = etfs_pd['Symbol']
etfs_pd = etfs_pd[['ETF Name', 'Prediction', 'Pesimistic', 'Optimistic']]

etfs_pd
ETF Name Prediction Pesimistic Optimistic
Symbol
MIDD.L iShares FTSE 250 UCITS ETF GBP (Dist) 0.03 0.00 0.05
CEBL.DE iShares VII PLC - iShares MSCI EM Asia ETF USD... 0.10 0.04 0.10
DBPD.DE Xtrackers ShortDAX x2 Daily Swap UCITS ETF 1C -0.05 -0.10 0.00
VUKE.DE Vanguard FTSE 100 UCITS ETF GBP Accumulation 0.08 0.05 0.10
XMOV.DE Xtrackers Future Mobility UCITS ETF1C 0.15 0.05 0.20
TRET.AS VanEck Vectors Global Real Estate UCITS ETF 0.03 0.00 0.05
TGBT.AS VanEck Vectors iBoxx EUR Sovereign Diversified... 0.05 0.03 0.07
DBX1.DE Xtrackers MSCI Emerging Markets Swap UCITS ETF 1C 0.05 0.00 0.07
IWDA.AS iShares Core MSCI World UCITS ETF USD (Acc) 0.05 0.02 0.07
CSPX.AS iShares Core S&P 500 UCITS ETF USD (Acc) 0.05 0.07 0.05
EXSA.DE iShares STOXX Europe 600 UCITS ETF (DE) 0.02 -0.02 0.04
EQQQ.DE Invesco EQQQ NASDAQ-100 UCITS ETF 0.10 0.05 0.15

I have the symbol, the ETF Name, and my views. My views express what I think it is going to happen with that ETF over the year. Later, I will take a step back, taking a pesimitic side and fill the matching row. Last, I will do my optimistic view.

# Get the historical data of the ETFs and exchange to EUR

etfs = pd.DataFrame()
cc = CurrencyConverter(fallback_on_missing_rate=True, fallback_on_wrong_date='True')

for symbol in etfs_pd.index:
    history = yf.download(symbol, start=START_DATE, end=END_DATE)['Adj Close']
    dataframe = pd.DataFrame(history)
    currency = yf.Ticker(symbol).info['currency'].upper()
    dataframe.columns = [ symbol + ':' + currency ]
    
    # Add column with security in EUR if needed
    if currency.lower != 'eur':
        for index in dataframe.index:
            original_value = dataframe.at[index, symbol + ':' + currency]
            value = cc.convert(original_value, currency, 'EUR', date=index.to_pydatetime())
            dataframe.at[index, symbol + ':EUR'] = value
    
    # Merge everything
    etfs = pd.concat([ etfs, dataframe], axis='columns')

# Fill any NA values with the best forward or backarwd value
etfs.fillna(method='ffill', inplace=True)

del cc, symbol, history, dataframe, currency, original_value, value, index
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
# Get market reference (MSCI All World Accumulative ETF in USD)

market = pd.DataFrame(yf.download(ALL_WORLD, start=START_DATE, end=END_DATE)['Adj Close'])
market.columns = ['Market Ref:USD']

cc = CurrencyConverter(fallback_on_missing_rate=True, fallback_on_wrong_date='True')

for index in market.index:
    original_value = market.at[index, 'Market Ref:USD']
    value = cc.convert(original_value, 'USD', 'EUR', date=index.to_pydatetime())
    market.at[index, 'Market Ref:EUR'] = value

# Fill any NA values with the best forward or backarwd value
market.fillna(method='ffill', inplace=True)

del cc, original_value, value, index
[*********************100%***********************]  1 of 1 completed
# Clean up dataframes and keep only EUR with the symbol name
# Same for Market

for column in etfs.columns:
    if ':EUR' not in column:
            etfs.drop(column, axis='columns', inplace=True)
    symbol, currency = column.split(':')
    etfs.rename(mapper={column: symbol}, axis='columns', inplace=True)

for column in market.columns:
    if ':EUR' not in column:
            market.drop(column, axis='columns', inplace=True)
    symbol, currency = column.split(':')
    market.rename(mapper={column: symbol}, axis='columns', inplace=True)

del column, symbol, currency
# Create a checkpoint

etfs.to_csv('cache_etfs.csv')
market.to_csv('cache_market.csv')
# Restore Checkpoint

etfs = pd.read_csv('cache_etfs.csv')
etfs.set_index(pd.DatetimeIndex(etfs['Date']), inplace=True)
etfs.drop(['Date'], axis='columns', inplace=True)

market = pd.read_csv('cache_market.csv')
market.set_index(pd.DatetimeIndex(market['Date']), inplace=True)
market.drop(['Date'], axis='columns', inplace=True)
# Construct the dictionary with your views
views = etfs_pd['Prediction'].to_dict()

# Construct a uncertanties dictionary
optimistic = etfs_pd['Optimistic'].to_dict()
pesimistic = etfs_pd['Pesimistic'].to_dict()

variances = []
for symbol in views.keys():
    sigma = (optimistic[symbol] - pesimistic[symbol])/2
    variances.append(sigma ** 2)

omega = np.diag(variances)

del optimistic, pesimistic, variances, symbol, sigma
# Construct the Black Litterman 

S = risk_models.CovarianceShrinkage(etfs).ledoit_wolf()

delta = black_litterman.market_implied_risk_aversion(market['Market Ref'])
# Using a pi equal weighted as the market caps for ETFs are too complex to calculate

bl = BlackLittermanModel(S, pi="equal", risk_aversion=delta,
                        absolute_views=views, omega=omega)
ret_bl = bl.bl_returns()
S_bl = bl.bl_cov()
from pypfopt import EfficientFrontier, objective_functions

ef = EfficientFrontier(ret_bl, S_bl)
ef.add_objective(objective_functions.L2_reg)
ef.max_sharpe()
weights = ef.clean_weights()
pd.Series(weights).plot.pie(figsize=(10,10));
/home/luis/.local/lib/python3.8/site-packages/pypfopt/efficient_frontier.py:195: UserWarning: max_sharpe transforms the optimisation problem so additional objectives may not work as expected.
  warnings.warn(

png

from pypfopt import DiscreteAllocation

da = DiscreteAllocation(weights, etfs.iloc[-1], total_portfolio_value=TOTAL_FUNDS)
alloc, leftover = da.greedy_portfolio()

allocation = pd.DataFrame(data=etfs_pd['ETF Name'], index=etfs_pd.index, columns=['ETF Name'])

for key in alloc.keys():
    allocation.at[key, 'Desired Position'] = alloc[key]
    allocation.at[key, 'Last Price EUR'] = int(etfs.iloc[-1][key] * 100)/100
    allocation.at[key, 'Total Funds EUR'] = int(etfs.iloc[-1][key] * alloc[key] * 100)/100

allocation.fillna(0, inplace=True)
allocation['Desired Position'] = allocation['Desired Position'].astype(int)
allocation
ETF Name Desired Position Last Price EUR Total Funds EUR
Symbol
MIDD.L iShares FTSE 250 UCITS ETF GBP (Dist) 0 0.00 0.00
CEBL.DE iShares VII PLC - iShares MSCI EM Asia ETF USD... 7 167.13 1169.97
DBPD.DE Xtrackers ShortDAX x2 Daily Swap UCITS ETF 1C 437 1.86 814.30
VUKE.DE Vanguard FTSE 100 UCITS ETF GBP Accumulation 34 33.43 1136.79
XMOV.DE Xtrackers Future Mobility UCITS ETF1C 22 57.91 1274.23
TRET.AS VanEck Vectors Global Real Estate UCITS ETF 6 33.00 198.03
TGBT.AS VanEck Vectors iBoxx EUR Sovereign Diversified... 66 14.74 973.36
DBX1.DE Xtrackers MSCI Emerging Markets Swap UCITS ETF 1C 17 47.34 804.78
IWDA.AS iShares Core MSCI World UCITS ETF USD (Acc) 12 60.79 729.59
CSPX.AS iShares Core S&P 500 UCITS ETF USD (Acc) 2 312.65 625.30
EXSA.DE iShares STOXX Europe 600 UCITS ETF (DE) 18 40.40 727.37
EQQQ.DE Invesco EQQQ NASDAQ-100 UCITS ETF 5 257.39 1286.99
allocation.to_excel('/mnt/wd-bigfoot/share/luis/OneDrive/Documents/Portfolio/99 Last Allocation of ETFs.xlsx', 
                    index=True)

And like that, I have allocate a collection of ETF, with my views and trying to maximize the return and minimize the risk.

In the next post, I will repeat this procedure for Stocks.