← Home

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}")
WORLD - ACWI - WORLD_EW - WORLD_EX_USA - USA - EMERGING - GOLD - GOLD_ - EURUSD - S&P500 - TBILL - TBOND - 
WORLD ACWI WORLD_EW WORLD_EX_USA USA EMERGING GOLD EURUSD S&P500 TBILL TBOND
1927-12-30 NaN NaN NaN NaN NaN NaN NaN NaN 17.660000 NaN NaN
1928-01-03 NaN NaN NaN NaN NaN NaN NaN NaN 17.760000 NaN NaN
1928-01-04 NaN NaN NaN NaN NaN NaN NaN NaN 17.719999 NaN NaN
1928-01-05 NaN NaN NaN NaN NaN NaN NaN NaN 17.549999 NaN NaN
1928-01-06 NaN NaN NaN NaN NaN NaN NaN NaN 17.660000 NaN NaN
... ... ... ... ... ... ... ... ... ... ... ...
2025-04-14 NaN NaN NaN NaN NaN NaN 3204.800049 1.134314 5405.970215 7.037596 12.528104
2025-04-15 NaN NaN NaN NaN NaN NaN 3218.699951 1.133967 5396.629883 7.038406 12.529588
2025-04-16 NaN NaN NaN NaN NaN NaN 3326.600098 1.129267 5275.700195 7.039217 12.531057
2025-04-17 NaN NaN NaN NaN NaN NaN 3308.699951 1.139679 5282.700195 7.040028 12.532545
2025-04-18 NaN NaN NaN NaN NaN NaN NaN 1.139601 NaN NaN NaN

24833 rows × 11 columns

WORLD          : 1998-12-31 - 2025-04-04
ACWI           : 1998-12-31 - 2025-04-04
WORLD_EW       : 1994-06-30 - 2025-04-04
WORLD_EX_USA   : 1998-12-31 - 2025-04-04
USA            : 1998-12-31 - 2025-04-04
EMERGING       : 1998-12-31 - 2025-04-04
GOLD           : 1968-03-22 - 2025-04-17
EURUSD         : 2003-12-01 - 2025-04-18
S&P500         : 1927-12-30 - 2025-04-17
TBILL          : 1960-01-04 - 2025-04-17
TBOND          : 1962-01-02 - 2025-04-17
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]:
WORLD ACWI WORLD_EW WORLD_EX_USA USA EMERGING GOLD EURUSD S&P500 TBILL TBOND pf1 pf2 pf3
1998-12-31 2293.188347 92.183817 1000.000000 2545.488004 2563.698904 86.833612 288.000000 NaN 1229.229980 4.973077 6.804584 1230.244257 1469.247425 1833.471595
1999-01-01 2293.188829 92.183836 1000.000092 2545.489099 2563.698904 86.833612 288.000000 NaN 1229.229980 4.973077 6.804584 1230.244498 1469.247666 1833.472033
1999-01-04 2316.363056 93.144102 1007.044289 2601.031087 2560.746266 88.326635 287.100000 NaN 1228.099976 4.973672 6.805456 1242.099519 1480.508818 1855.011642
1999-01-05 2339.967784 94.090737 1011.160061 2619.898855 2594.333234 89.194115 286.700000 NaN 1244.780029 4.974267 6.806338 1254.082126 1495.676627 1872.768335
1999-01-06 2384.283305 95.884042 1026.985970 2660.671591 2652.141901 91.248288 287.500000 NaN 1272.339966 4.974863 6.807218 1277.016139 1523.980671 1906.910864
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2025-03-31 11520.918542 449.950009 6198.348541 8750.298947 16030.537281 590.687458 3122.800049 1.082392 5611.850098 7.029521 12.513540 6562.225518 8047.141755 8739.698260
2025-04-01 11587.605619 452.678888 6235.061813 8838.866022 16096.305070 595.686022 3118.899902 1.081888 5633.069824 7.030330 12.514965 6596.288597 8086.781899 8795.465124
2025-04-02 11648.404113 454.857955 6257.128719 8842.810972 16210.998798 596.322204 3139.899902 1.079599 5670.970215 7.031140 12.516403 6631.078698 8132.914137 8833.678459
2025-04-03 11217.756431 439.414401 6136.175487 8790.851238 15415.735634 591.706011 3097.000000 1.090869 5396.520020 7.031946 12.517794 6405.790019 7829.022380 8569.102388
2025-04-04 10563.245147 415.855243 5817.685620 8310.794034 14492.882226 583.582512 3012.000000 1.104374 5074.080078 7.032747 12.519161 6059.097327 7391.669047 8090.098784

6853 rows × 14 columns

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]:
WORLD ACWI WORLD_EW WORLD_EX_USA USA EMERGING GOLD EURUSD S&P500 TBILL TBOND pf1 pf2 pf3
count 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 4788.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000
mean 6.437722 6.297232 7.362510 4.922990 7.408241 7.708862 9.592482 -0.785559 6.026435 1.071958 2.212856 6.571675 6.704636 6.177535
std 8.920265 8.761535 8.942967 9.532631 9.427099 12.992010 10.017877 3.350492 9.066193 0.943404 0.786237 8.159962 8.239309 8.689616
min -18.673643 -18.700632 -18.808042 -20.414552 -17.766691 -17.998153 -15.365120 -8.861288 -19.115461 0.022508 0.991488 -17.380881 -17.447671 -18.489984
25% 3.273532 2.915562 2.394448 -0.344897 1.907965 -0.826730 2.511939 -3.088620 0.739820 0.141261 1.541648 3.051728 3.690427 2.293832
50% 8.157505 7.739529 7.405514 4.869065 10.117315 4.556428 11.238450 -0.761094 8.767674 0.922481 2.015822 7.834305 8.375779 7.494266
75% 12.461089 11.930805 11.914368 9.608422 13.714730 11.392019 16.578663 1.271774 12.022077 1.769404 2.985515 11.461693 12.101593 11.381155
max 25.431917 26.395339 35.004598 32.686733 28.381186 47.182360 36.218371 9.979711 26.202445 3.213217 3.891303 25.574494 25.561295 26.919984
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
100%|██████████| 1771/1771 [00:01<00:00, 1478.66it/s]
Out[10]:
USA WORLD_EX_USA EMERGING GOLD U100X00E00G00 U95X00E00G05 U90X00E00G10 U85X00E00G15 U80X00E00G20 U75X00E00G25 ... U10X90E00G00 U05X90E00G05 U00X90E00G10 U05X90E05G00 U00X90E05G05 U00X90E10G00 U05X95E00G00 U00X95E00G05 U00X95E05G00 U00X100E00G00
1998-12-31 2563.698904 2545.488004 86.833612 288.000000 2563.698904 2449.913959 2336.129014 2222.344068 2108.559123 1994.774178 ... 2547.309094 2433.524149 2319.739204 2423.465830 2309.680884 2299.622565 2546.398549 2432.613604 2422.555285 2545.488004
1999-01-01 2563.698904 2545.489099 86.833612 288.000000 2563.698904 2449.913959 2336.129014 2222.344068 2108.559123 1994.774178 ... 2547.310079 2433.525134 2319.740189 2423.466815 2309.681869 2299.623550 2546.399589 2432.614644 2422.556324 2545.489099
1999-01-04 2560.746266 2601.031087 88.326635 287.100000 2560.746266 2447.063953 2333.381640 2219.699326 2106.017013 1992.334700 ... 2597.002605 2483.320292 2369.637978 2473.381623 2359.699310 2349.760642 2599.016846 2485.334533 2475.395864 2601.031087
1999-01-05 2594.333234 2619.898855 89.194115 286.700000 2594.333234 2478.951573 2363.569911 2248.188249 2132.806587 2017.424926 ... 2617.342293 2501.960631 2386.578970 2492.085337 2376.703675 2366.828381 2618.620574 2503.238912 2493.363618 2619.898855
1999-01-06 2652.141901 2660.671591 91.248288 287.500000 2652.141901 2533.909806 2415.677711 2297.445616 2179.213521 2060.981426 ... 2659.818622 2541.586527 2423.354432 2531.773941 2413.541846 2403.729261 2660.245106 2542.013011 2532.200426 2660.671591
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2025-03-31 16030.537281 8750.298947 590.687458 3122.800049 16030.537281 15385.150419 14739.763557 14094.376696 13448.989834 12803.602973 ... 9478.322781 8832.935919 8187.549057 8706.330289 8060.943428 7934.337798 9114.310864 8468.924002 8342.318373 8750.298947
2025-04-01 16096.305070 8838.866022 595.686022 3118.899902 16096.305070 15447.434812 14798.564553 14149.694295 13500.824036 12851.953778 ... 9564.609926 8915.739668 8266.869410 8789.578974 8140.708716 8014.548022 9201.737974 8552.867716 8426.707022 8838.866022
2025-04-02 16210.998798 8842.810972 596.322204 3139.899902 16210.998798 15557.443853 14903.888909 14250.333964 13596.779019 12943.224074 ... 9579.629754 8926.074810 8272.519865 8798.895925 8145.340980 8018.162095 9211.220363 8557.665418 8430.486533 8842.810972
2025-04-03 15415.735634 8790.851238 591.706011 3097.000000 15415.735634 14799.798853 14183.862071 13567.925289 12951.988507 12336.051726 ... 9453.339678 8837.402896 8221.466115 8712.138197 8096.201415 7970.936716 9122.095458 8506.158676 8380.893977 8790.851238
2025-04-04 14492.882226 8310.794034 583.582512 3012.000000 14492.882226 13918.838115 13344.794003 12770.749892 12196.705781 11622.661669 ... 8929.002853 8354.958742 7780.914631 8233.537867 7659.493756 7538.072882 8619.898444 8045.854332 7924.433458 8310.794034

6853 rows × 1775 columns

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]:
USA WORLD_EX_USA EMERGING GOLD U100X00E00G00 U95X00E00G05 U90X00E00G10 U85X00E00G15 U80X00E00G20 U75X00E00G25 ... U10X90E00G00 U05X90E00G05 U00X90E00G10 U05X90E05G00 U00X90E05G05 U00X90E10G00 U05X95E00G00 U00X95E00G05 U00X95E05G00 U00X100E00G00
count 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 ... 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000 6070.000000
mean 7.408241 4.922990 7.708862 9.592482 7.408241 7.408036 7.408723 7.410512 7.413665 7.418510 ... 5.228517 5.117600 4.994039 5.092647 4.966011 4.937826 5.078221 4.956850 4.930036 4.922990
std 9.427099 9.532631 12.992010 10.017877 9.427099 9.271418 9.108387 8.937725 8.759221 8.572788 ... 9.332593 9.340596 9.367675 9.424644 9.451326 9.538143 9.427683 9.452293 9.535199 9.532631
min -17.766691 -20.414552 -17.998153 -15.365120 -17.766691 -17.619570 -17.458259 -17.280602 -17.083974 -16.865163 ... -20.076409 -20.067383 -20.057551 -20.231074 -20.228548 -20.401071 -20.245940 -20.244173 -20.408148 -20.414552
25% 1.907965 -0.344897 -0.826730 2.511939 1.907965 2.090634 2.217519 2.402517 2.675958 2.929081 ... 0.513734 0.066775 -0.231142 0.090737 -0.277435 -0.328088 0.083171 -0.287965 -0.348003 -0.344897
50% 10.117315 4.869065 4.556428 11.238450 10.117315 10.057987 10.003206 9.940737 9.881254 9.794454 ... 5.593721 5.282096 4.938934 5.241498 4.904644 4.873819 5.224073 4.918366 4.877019 4.869065
75% 13.714730 9.608422 11.392019 16.578663 13.714730 13.642376 13.571849 13.500105 13.349126 13.156726 ... 10.050312 9.776093 9.476386 9.811689 9.552037 9.598771 9.791236 9.569855 9.587280 9.608422
max 28.381186 32.686733 47.182360 36.218371 28.381186 28.234087 28.073866 27.898686 27.706345 27.494192 ... 31.369369 31.843142 32.355183 32.034895 32.558426 32.764619 32.030334 32.527967 32.723750 32.686733

8 rows × 1775 columns

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()
Starting from 2002-01-01
Starting from 2004-01-01
Starting from 2005-12-31
Starting from 2009-12-30
Starting from 2011-12-30
← Home