This is a continuation of the previous post. Here I will apply the Black Litterman model to a portfolio of stocks.
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'
STOCKS_SHEET='Stocks'
START_DATE='2010-01-01'
END_DATE='2021-01-08'
ALL_WORLD='IWDA.AS'
TOTAL_FUNDS=40e3 # 40.000 EUR
# Open the portfolio and get the stocks
stocks_pd = pd.read_excel(PORTFOLIO, engine='openpyxl', sheet_name=STOCKS_SHEET)
stocks_pd.index = stocks_pd['Symbol']
stocks_pd = stocks_pd[['Company Name', 'Prediction', 'Pesimistic', 'Optimistic']]
stocks_pd
|
Company Name |
Prediction |
Pesimistic |
Optimistic |
Symbol |
|
|
|
|
AMD |
Advanced Micro Devices, Inc. |
0.20 |
0.03 |
0.40 |
AIR.PA |
Airbus SE |
0.10 |
0.03 |
0.20 |
GOOGL |
Alphabet Inc. |
0.10 |
0.03 |
0.30 |
AMZN |
Amazon.com, Inc. |
0.20 |
0.05 |
0.50 |
AAPL |
Apple Inc. |
0.15 |
0.00 |
0.40 |
BA.L |
BAE Systems plc |
0.00 |
-0.05 |
0.05 |
CSCO |
Cisco Systems, Inc. |
0.00 |
-0.05 |
0.05 |
BN.PA |
Danone S.A. |
0.00 |
-0.05 |
0.05 |
FB |
Facebook, Inc. |
0.05 |
-0.05 |
0.05 |
GE |
General Electric Company |
0.05 |
0.00 |
0.05 |
HYU.F |
Hyundai Corporation |
0.10 |
0.00 |
0.30 |
IDR.MC |
Indra Sistemas, S.A. |
0.00 |
-0.05 |
0.02 |
INTC |
Intel Corporation |
0.00 |
-0.02 |
0.05 |
IBM |
International Business Machines Corporation |
0.05 |
-0.02 |
0.10 |
IAG.MC |
International Consolidated Airlines Group, S.A. |
0.50 |
-0.10 |
1.00 |
PHIA.AS |
Koninklijke Philips N.V. |
0.00 |
-0.05 |
0.05 |
MEL.MC |
Meliá Hotels International, S.A. |
0.20 |
0.00 |
0.50 |
MRK |
Merck & Co., Inc. |
0.05 |
0.00 |
0.05 |
MSFT |
Microsoft Corporation |
0.10 |
0.00 |
0.15 |
NESN.SW |
Nestlé S.A. |
0.00 |
-0.05 |
0.03 |
NFLX |
Netflix, Inc. |
0.05 |
-0.01 |
0.15 |
NHH.MC |
NH Hotel Group, S.A. |
0.20 |
-0.05 |
0.50 |
NOKIA.HE |
Nokia Corporation |
0.05 |
-0.05 |
0.20 |
NVDA |
NVIDIA Corporation |
0.10 |
-0.05 |
0.30 |
ORCL |
Oracle Corporation |
0.05 |
0.00 |
0.05 |
PEP |
PepsiCo, Inc. |
0.00 |
-0.05 |
0.05 |
QCOM |
QUALCOMM Incorporated |
0.05 |
-0.02 |
0.15 |
SSUN.F |
Samsung Electronics Co., Ltd. |
0.10 |
-0.02 |
0.20 |
SIE.DE |
Siemens Aktiengesellschaft |
0.05 |
-0.02 |
0.10 |
ENR.DE |
Siemens Energy AG |
0.05 |
-0.02 |
0.10 |
SPOT |
Spotify Technology S.A. |
0.02 |
-0.05 |
0.10 |
TSLA |
Tesla, Inc. |
0.00 |
-0.20 |
0.05 |
TXN |
Texas Instruments Incorporated |
0.05 |
0.00 |
0.10 |
BA |
The Boeing Company |
0.05 |
-0.05 |
0.10 |
KHC |
The Kraft Heinz Company |
0.00 |
-0.05 |
0.05 |
PG |
The Procter & Gamble Company |
0.05 |
-0.05 |
0.05 |
DIS |
The Walt Disney Company |
0.05 |
-0.05 |
0.20 |
TMO |
Thermo Fisher Scientific Inc. |
0.05 |
-0.05 |
0.15 |
TM |
Toyota Motor Corporation |
0.05 |
-0.02 |
0.10 |
UBER |
Uber Technologies, Inc. |
0.10 |
-0.10 |
0.20 |
UL |
Unilever PLC |
0.00 |
-0.05 |
0.05 |
# Get the historical data of the stocks and exchange to EUR
stocks = pd.DataFrame()
cc = CurrencyConverter(fallback_on_missing_rate=True, fallback_on_wrong_date='True')
for symbol in stocks_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 Market Cap to the stocks_pd
stocks_pd.at[symbol, 'Market Cap'] = cc.convert(yf.Ticker(symbol).info['marketCap'],
currency, 'EUR',
dataframe.index[-1].to_pydatetime())
# 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
stocks = pd.concat([ stocks, dataframe], axis='columns')
# Fill any NA values with the best forward or backarwd value
stocks.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
[*********************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
[*********************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
[*********************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
|
Company Name |
Prediction |
Pesimistic |
Optimistic |
Market Cap |
Symbol |
|
|
|
|
|
AMD |
Advanced Micro Devices, Inc. |
0.20 |
0.03 |
0.40 |
8.148238e+10 |
AIR.PA |
Airbus SE |
0.10 |
0.03 |
0.20 |
7.461750e+10 |
GOOGL |
Alphabet Inc. |
0.10 |
0.03 |
0.30 |
1.183262e+12 |
AMZN |
Amazon.com, Inc. |
0.20 |
0.05 |
0.50 |
1.355935e+12 |
AAPL |
Apple Inc. |
0.15 |
0.00 |
0.40 |
1.765546e+12 |
BA.L |
BAE Systems plc |
0.00 |
-0.05 |
0.05 |
1.787769e+10 |
CSCO |
Cisco Systems, Inc. |
0.00 |
-0.05 |
0.05 |
1.811978e+11 |
BN.PA |
Danone S.A. |
0.00 |
-0.05 |
0.05 |
3.849391e+10 |
FB |
Facebook, Inc. |
0.05 |
-0.05 |
0.05 |
7.102801e+11 |
GE |
General Electric Company |
0.05 |
0.00 |
0.05 |
9.526443e+10 |
HYU.F |
Hyundai Corporation |
0.10 |
0.00 |
0.30 |
3.768063e+10 |
IDR.MC |
Indra Sistemas, S.A. |
0.00 |
-0.05 |
0.02 |
1.320735e+09 |
INTC |
Intel Corporation |
0.00 |
-0.02 |
0.05 |
2.213519e+11 |
IBM |
International Business Machines Corporation |
0.05 |
-0.02 |
0.10 |
1.000500e+11 |
IAG.MC |
International Consolidated Airlines Group, S.A. |
0.50 |
-0.10 |
1.00 |
1.080973e+10 |
PHIA.AS |
Koninklijke Philips N.V. |
0.00 |
-0.05 |
0.05 |
4.355928e+10 |
MEL.MC |
Meliá Hotels International, S.A. |
0.20 |
0.00 |
0.50 |
1.345669e+09 |
MRK |
Merck & Co., Inc. |
0.05 |
0.00 |
0.05 |
1.683183e+11 |
MSFT |
Microsoft Corporation |
0.10 |
0.00 |
0.15 |
1.537831e+12 |
NESN.SW |
Nestlé S.A. |
0.00 |
-0.05 |
0.03 |
2.751414e+11 |
NFLX |
Netflix, Inc. |
0.05 |
-0.01 |
0.15 |
2.033889e+11 |
NHH.MC |
NH Hotel Group, S.A. |
0.20 |
-0.05 |
0.50 |
1.475451e+09 |
NOKIA.HE |
Nokia Corporation |
0.05 |
-0.05 |
0.20 |
1.926819e+10 |
NVDA |
NVIDIA Corporation |
0.10 |
-0.05 |
0.30 |
2.806550e+11 |
ORCL |
Oracle Corporation |
0.05 |
0.00 |
0.05 |
1.641228e+11 |
PEP |
PepsiCo, Inc. |
0.00 |
-0.05 |
0.05 |
1.651222e+11 |
QCOM |
QUALCOMM Incorporated |
0.05 |
-0.02 |
0.15 |
1.291990e+11 |
SSUN.F |
Samsung Electronics Co., Ltd. |
0.10 |
-0.02 |
0.20 |
4.059750e+11 |
SIE.DE |
Siemens Aktiengesellschaft |
0.05 |
-0.02 |
0.10 |
1.086902e+11 |
ENR.DE |
Siemens Energy AG |
0.05 |
-0.02 |
0.10 |
2.160643e+10 |
SPOT |
Spotify Technology S.A. |
0.02 |
-0.05 |
0.10 |
4.376889e+10 |
TSLA |
Tesla, Inc. |
0.00 |
-0.20 |
0.05 |
5.454659e+11 |
TXN |
Texas Instruments Incorporated |
0.05 |
0.00 |
0.10 |
1.416198e+11 |
BA |
The Boeing Company |
0.05 |
-0.05 |
0.10 |
1.208403e+11 |
KHC |
The Kraft Heinz Company |
0.00 |
-0.05 |
0.05 |
4.085665e+10 |
PG |
The Procter & Gamble Company |
0.05 |
-0.05 |
0.05 |
2.802307e+11 |
DIS |
The Walt Disney Company |
0.05 |
-0.05 |
0.20 |
2.940217e+11 |
TMO |
Thermo Fisher Scientific Inc. |
0.05 |
-0.05 |
0.15 |
1.527621e+11 |
TM |
Toyota Motor Corporation |
0.05 |
-0.02 |
0.10 |
1.805378e+11 |
UBER |
Uber Technologies, Inc. |
0.10 |
-0.10 |
0.20 |
8.531728e+10 |
UL |
Unilever PLC |
0.00 |
-0.05 |
0.05 |
1.977436e+11 |
# 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 stocks.columns:
if ':EUR' not in column:
stocks.drop(column, axis='columns', inplace=True)
symbol, currency = column.split(':')
stocks.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
stocks.to_csv('cache_stocks.csv')
market.to_csv('cache_market.csv')
# Restore Checkpoint
stocks = pd.read_csv('cache_stocks.csv')
stocks.set_index(pd.DatetimeIndex(stocks['Date']), inplace=True)
stocks.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 = stocks_pd['Prediction'].to_dict()
# Construct a uncertanties dictionary
optimistic = stocks_pd['Optimistic'].to_dict()
pesimistic = stocks_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(stocks).ledoit_wolf()
delta = black_litterman.market_implied_risk_aversion(market['Market Ref'])
# Using a pi equal weighted as the market caps for stocks are too complex to calculate
bl = BlackLittermanModel(S, pi="market", market_caps=stocks_pd['Market Cap'],
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(
/home/luis/.local/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py:1547: MatplotlibDeprecationWarning: normalize=None does not normalize if the sum is less than 1 but this behavior is deprecated since 3.3 until two minor releases later. After the deprecation period the default value will be normalize=True. To prevent normalization pass normalize=False
results = ax.pie(y, labels=blabels, **kwds)
from pypfopt import DiscreteAllocation
da = DiscreteAllocation(weights, stocks.iloc[-1], total_portfolio_value=TOTAL_FUNDS)
alloc, leftover = da.greedy_portfolio()
allocation = pd.DataFrame(data=stocks_pd['Company Name'],
index=stocks_pd.index, columns=['Company Name'])
for key in alloc.keys():
allocation.at[key, 'Desired Position'] = alloc[key]
allocation.at[key, 'Last Price EUR'] = int(stocks.iloc[-1][key] * 100)/100
allocation.at[key, 'Total Funds EUR'] = int(stocks.iloc[-1][key] * alloc[key] * 100)/100
allocation.fillna(0, inplace=True)
allocation['Desired Position'] = allocation['Desired Position'].astype(int)
allocation
|
Company Name |
Desired Position |
Last Price EUR |
Total Funds EUR |
Symbol |
|
|
|
|
AMD |
Advanced Micro Devices, Inc. |
46 |
81.66 |
3756.74 |
AIR.PA |
Airbus SE |
19 |
89.62 |
1702.78 |
GOOGL |
Alphabet Inc. |
2 |
1522.77 |
3045.55 |
AMZN |
Amazon.com, Inc. |
2 |
2713.83 |
5427.66 |
AAPL |
Apple Inc. |
28 |
112.19 |
3141.34 |
BA.L |
BAE Systems plc |
0 |
0.00 |
0.00 |
CSCO |
Cisco Systems, Inc. |
0 |
0.00 |
0.00 |
BN.PA |
Danone S.A. |
0 |
0.00 |
0.00 |
FB |
Facebook, Inc. |
7 |
230.63 |
1614.46 |
GE |
General Electric Company |
80 |
9.66 |
773.20 |
HYU.F |
Hyundai Corporation |
29 |
34.29 |
994.69 |
IDR.MC |
Indra Sistemas, S.A. |
0 |
0.00 |
0.00 |
INTC |
Intel Corporation |
0 |
0.00 |
0.00 |
IBM |
International Business Machines Corporation |
5 |
109.24 |
546.21 |
IAG.MC |
International Consolidated Airlines Group, S.A. |
374 |
1.73 |
649.82 |
PHIA.AS |
Koninklijke Philips N.V. |
0 |
0.00 |
0.00 |
MEL.MC |
Meliá Hotels International, S.A. |
237 |
5.53 |
1311.79 |
MRK |
Merck & Co., Inc. |
12 |
71.47 |
857.75 |
MSFT |
Microsoft Corporation |
13 |
186.91 |
2429.83 |
NESN.SW |
Nestlé S.A. |
0 |
0.00 |
0.00 |
NFLX |
Netflix, Inc. |
3 |
436.74 |
1310.22 |
NHH.MC |
NH Hotel Group, S.A. |
159 |
3.46 |
550.14 |
NOKIA.HE |
Nokia Corporation |
48 |
3.23 |
155.20 |
NVDA |
NVIDIA Corporation |
5 |
457.92 |
2289.63 |
ORCL |
Oracle Corporation |
15 |
54.12 |
811.92 |
PEP |
PepsiCo, Inc. |
0 |
0.00 |
0.00 |
QCOM |
QUALCOMM Incorporated |
9 |
132.99 |
1196.92 |
SSUN.F |
Samsung Electronics Co., Ltd. |
1 |
1382.00 |
1382.00 |
SIE.DE |
Siemens Aktiengesellschaft |
2 |
124.30 |
248.60 |
ENR.DE |
Siemens Energy AG |
0 |
0.00 |
0.00 |
SPOT |
Spotify Technology S.A. |
0 |
0.00 |
0.00 |
TSLA |
Tesla, Inc. |
2 |
700.34 |
1400.68 |
TXN |
Texas Instruments Incorporated |
5 |
143.26 |
716.30 |
BA |
The Boeing Company |
7 |
182.55 |
1277.86 |
KHC |
The Kraft Heinz Company |
0 |
0.00 |
0.00 |
PG |
The Procter & Gamble Company |
0 |
0.00 |
0.00 |
DIS |
The Walt Disney Company |
6 |
153.26 |
919.56 |
TMO |
Thermo Fisher Scientific Inc. |
2 |
429.14 |
858.29 |
TM |
Toyota Motor Corporation |
4 |
131.02 |
524.09 |
UBER |
Uber Technologies, Inc. |
2 |
48.17 |
96.34 |
UL |
Unilever PLC |
0 |
0.00 |
0.00 |
allocation.to_excel('/mnt/wd-bigfoot/share/luis/OneDrive/Documents/Portfolio/99 Last Allocation of Stocks.xlsx',
index=True)