First exploring¶
Starting from: https://www.youtube.com/watch?v=bjZq9Fq5Wtg
AIM: find the best portfolio with VT + EXUS + EME
In [ ]:
import pandas as pd
import numpy as np
from tqdm import tqdm
import copy
import datetime
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
pd.set_option('plotting.backend', 'plotly')
pio.renderers.default = 'notebook_connected+vscode' # plotly>6.0.1
In [2]:
urlo = "https://raw.githubusercontent.com/paolocole/Stock-Indexes-Historical-Data/main/DAILY/NET/USD/"
elenco = {
"WORLD":"DEVELOPED-MARKETS-DM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/WORLD.csv",
"ACWI":"ALL-COUNTRY-DM-EM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/ACWI.csv",
"WORLD_EW":"DEVELOPED-MARKETS-DM/Region/NONE/EQUAL-WEIGHTED/STANDARD-LARGE-MID-CAP/WORLD-EQUAL-WEIGHTED.csv",
"WORLD_EX_USA": "DEVELOPED-MARKETS-DM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/WORLD-EX-USA.csv",
"USA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/USA.csv",
"EMERGING": "EMERGING-MARKETS-EM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/EM-EMERGING-MARKETS.csv",
"GOLD": "EXTRA/GOLD.csv",
"GOLD_": "GC=F",
"EURUSD": "EURUSD=X",
"S&P500": "^GSPC",
"TBILL": "^IRX",
"TBOND": "^TNX",
}
In [3]:
dati = pd.DataFrame()
for label, path in elenco.items():
print(label, end=" - ")
if path.endswith("csv"):
df = pd.read_csv(urlo + path, index_col=0)
df.index = pd.to_datetime(df.index).date
df.rename(columns={df.columns[0]:label}, inplace=True)
else:
ss = yf.Ticker(path).history(period="max", interval="1d")['Close']
ss = ss.rename(label)
ss.index = ss.index.date
df = ss.to_frame()
dati = dati.join(df, how='outer')
print()
# replace "GOLD" with the "GOLD_" since when available: the one from paolocole have weird splikes
dati["GOLD"] = dati["GOLD"].where(dati.index < dati["GOLD_"].first_valid_index(), dati["GOLD_"])
dati.drop(columns=["GOLD_"], inplace=True)
# TBILL: set the first available value to 1 and compute the cumulative returns
for tb in ["TBILL", "TBOND"]:
tb_start = dati[tb].first_valid_index()
dati.at[tb_start, tb] = 1
dati.loc[tb_start:, tb] = (1 + dati.loc[tb_start:, tb]/100/365).cumprod()
display(dati)
# for each column print the date of the first non-null value
for col in dati.columns:
first_date = dati[col].first_valid_index()
last_date = dati[col].last_valid_index()
print(f"{col:15s}: {first_date} - {last_date}")
In [4]:
firstdate = dati["WORLD"].first_valid_index()
lastdate = dati["WORLD"].last_valid_index()
In [5]:
(dati/dati.loc[firstdate]).plot().update_yaxes(type="log").update_layout(width=1000)
In [6]:
portolios = {
"pf1": {"WORLD": 0.5, "EMERGING": 0.3, "GOLD": 0.2},
"pf2": {"WORLD": 0.5, "EMERGING": 0.1, "GOLD": 0.2, "USA": 0.1},
"pf3": {"WORLD_EX_USA": 0.4, "USA": 0.3, "EMERGING": 0.2, "GOLD": 0.1},
}
dati_pf = dati.loc[firstdate:lastdate].ffill().copy()
for pf, weights in portolios.items():
dati_pf[pf] = 0
for col, weight in weights.items():
dati_pf[pf] += dati_pf[col] * weight
dati_pf
Out[6]:
In [7]:
(dati_pf/dati_pf.iloc[0]).plot().update_yaxes(type="log").update_layout(width=1000)
In [8]:
HOLDING_YEARS = 3
gain_pf = ( 1+dati_pf.pct_change(HOLDING_YEARS*261,fill_method=None) )**(1/HOLDING_YEARS) -1
(100*gain_pf).describe()
Out[8]:
In [9]:
gain_pf.dropna().plot()
Grid Screening for the optimal portfolio¶
In [10]:
grid_portfolio = {}
# make a combination of "WORLD_EX_USA", "EMERGING" "GOLD" "USA" with a step of 5%
for x in range(0, 101, 5):
for e in range(0, 101-x, 5):
for g in range(0, 101-x-e, 5):
u = 100 - x - e - g
if u < 0:
continue
grid_portfolio[f"U{u:02d}X{x:02d}E{e:02d}G{g:02d}"] = {
"USA": u/100,
"WORLD_EX_USA": x/100,
"EMERGING": e/100,
"GOLD": g/100,
}
dati_pfs = dati.loc[firstdate:lastdate][["USA", "WORLD_EX_USA", "EMERGING", "GOLD"]].ffill()
pf_data = {}
for pf, weights in tqdm(grid_portfolio.items()):
pf_data[pf] = sum(dati_pfs[col] * weight for col, weight in weights.items())
dati_pfs = pd.concat([dati_pfs, pd.DataFrame(pf_data, index=dati_pfs.index)], axis=1)
dati_pfs
Out[10]:
In [11]:
HOLDING_YEARS = 3
gain_pfs = ( 1+dati_pfs.pct_change(HOLDING_YEARS*261,fill_method=None) )**(1/HOLDING_YEARS) -1
(100*gain_pfs).describe()
Out[11]:
In [12]:
starting_dates = [
gain_pfs.first_valid_index(),
gain_pfs.first_valid_index() + datetime.timedelta(days=365*2),
gain_pfs.first_valid_index() + datetime.timedelta(days=365*4),
gain_pfs.first_valid_index() + datetime.timedelta(days=365*8),
gain_pfs.first_valid_index() + datetime.timedelta(days=365*10),
]
for start in starting_dates:
print(f"Starting from {start}")
mean = gain_pfs.query("index>@start").mean()
std = gain_pfs.query("index>@start").std()
fig = px.scatter(
x=std,
y=mean,
color=[ int(x.split("G")[1]) if x in grid_portfolio else 0 for x in std.index],
hover_data=[std.index],
)
fig.update_layout(
xaxis_title="Standard Deviation",
yaxis_title="Mean",
#xaxis_range=[0.03, 0.10],
#yaxis_range=[0.03, 0.15],
width=1000)
fig.show()