Analisi MSCI WORLD EQUAL WEIGHTEDΒΆ
- Link to video: https://www.youtube.com/watch?v=bjZq9Fq5Wtg
- Link to original notebook: https://colab.research.google.com/drive/1bT8zQX1B8KHZiH1UB1r8NKfRgw7OM5kN
- My path: Finance/coletti/2025_04_msci_world_equal_weighted.ipynb
Acronyms:
- MCW: Market Capitalization Weighted
- SEW: Stock Equal Weighted
- CEW: Country Equal Weighted
- DEV: Developed Markets
- ALL: Developed and Emerging Markets
AIM: understand is the SEW and CEW indices are better than MCW in term of risk and return on a medium-long term horizon, for the investor who does not look at shorter-period fluctuations. This is the thesis of the video and the mainly recurrent thesis of Coletti I don't agree with.
LIMITATIONS: I'm not considering the higher TER and transaction cost associated to EW vs MCW, nor the fact that in Italy I can not compenstate for ETFs the losses with the gains (e.g., if in a a CEW half countries are up and half are down, I will pay taxes on the gains, but I will not be able to compenstate with the losses of the other half).
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.io as pio
pio.renderers.default = 'notebook_connected+vscode' # plotly>6.0.1
urlo = "https://raw.githubusercontent.com/paolocole/Stock-Indexes-Historical-Data/main/DAILY/NET/EUR/"
elenco = {
"DEV-MCW":"DEVELOPED-MARKETS-DM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/WORLD.csv",
"ALL-MCW":"ALL-COUNTRY-DM-EM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/ACWI.csv",
#"ACWI IMI":"ALL-COUNTRY-DM-EM/Region/NONE/NONE/IMI-LARGE-MID-SMALL-CAP/ACWI-IMI.csv", # EXCLUDED: final resuts are so similar to ACWI that you shoud include extra fee (TER+transactions) to really appreciate the difference
"DEV-SEW":"DEVELOPED-MARKETS-DM/Region/NONE/EQUAL-WEIGHTED/STANDARD-LARGE-MID-CAP/WORLD-EQUAL-WEIGHTED.csv" # All stocks in developed markets (23) equally weighted and rebalanced quarterly
}
elenco_ew = {
"USA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/USA.csv",
"INDIA":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/INDIA.csv",
"JAPAN":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/JAPAN.csv",
"CHINA":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/CHINA.csv",
"BRAZIL":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/BRAZIL.csv",
"CHINA A":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/CHINA-A.csv",
"UK":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/UNITED-KINGDOM.csv",
"NORDIC":"DEVELOPED-MARKETS-DM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/NORDIC-COUNTRIES.csv",
"CANADA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/CANADA.csv",
"TAIWAN":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/TAIWAN.csv",
"MEXICO":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/MEXICO.csv",
"AUSTRALIA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/AUSTRALIA.csv",
"SWITZERLAND":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SWITZERLAND.csv",
"KOREA":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/KOREA.csv",
"SINGAPORE":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SINGAPORE.csv",
"SOUTH AFRICA":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SOUTH-AFRICA.csv",
"FRANCE":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/FRANCE.csv",
"GERMANY":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/GERMANY.csv",
"ITALY":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/ITALY.csv",
"SPAIN":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SPAIN.csv",
"POLAND":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/POLAND.csv",
"GREECE":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/GREECE.csv",
"INDONESIA":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/INDONESIA.csv",
"TURKEY":"EMERGING-MARKETS-EM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/TURKEY.csv",
"RUSSIA":"FRONTIER-MARKETS-FM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/RUSSIA-GROWTH.csv"
}
elenco_ew_dev = {
"USA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/USA.csv",
"JAPAN":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/JAPAN.csv",
"UK":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/UNITED-KINGDOM.csv",
"NORDIC":"DEVELOPED-MARKETS-DM/Region/NONE/NONE/STANDARD-LARGE-MID-CAP/NORDIC-COUNTRIES.csv",
"CANADA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/CANADA.csv",
"AUSTRALIA":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/AUSTRALIA.csv",
"SWITZERLAND":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SWITZERLAND.csv",
"SINGAPORE":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SINGAPORE.csv",
"FRANCE":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/FRANCE.csv",
"GERMANY":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/GERMANY.csv",
"ITALY":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/ITALY.csv",
"SPAIN":"DEVELOPED-MARKETS-DM/Country/NONE/NONE/STANDARD-LARGE-MID-CAP/SPAIN.csv"
}
dati = pd.DataFrame()
for nome in elenco:
print(nome, end=" - ")
df = pd.read_csv(urlo + elenco[nome], index_col=0)
df.index = pd.to_datetime(df.index)
df.rename(columns={df.columns[0]:nome}, inplace=True)
# df.dropna(inplace=True)
dati = pd.concat([dati,df], axis=1)
dati.dropna(inplace=True)
dati
ALL-CEW: all-countries (25) equally weighted withOUT rebalancingΒΆ
dati_ew = pd.DataFrame()
for nome in elenco_ew:
print(nome, end=" - ")
df = pd.read_csv(urlo + elenco_ew[nome], index_col=0)
df.index = pd.to_datetime(df.index)
df.rename(columns={df.columns[0]:nome}, inplace=True)
dati_ew = pd.concat([dati_ew,df], axis=1)
if "RUSSIA" in dati_ew.index: # devo sistemare la Russia a 1/10 del suo ultimo valore per non barare, se la lasciassi missing verrebbero nascosti i periodi in cui Γ¨ azzerata
dati_ew.loc["2022-03-09":,"RUSSIA"] = (dati_ew["RUSSIA"].loc["2022"].iloc[0])/20
# dati_ew.dropna(inplace=True) # non elimino qui i missing perchΓ© tanto quando farΓ² la media i paesi che non esistevano verranno automaticamente esclusi
# invece elimino i dati che non ho in quelli PRIMA
dati_ew = dati_ew.loc[dati.index[0]:]
display(dati_ew)
dati["ALL-CEW"] = (dati_ew/dati_ew.iloc[0]).mean(axis=1)
DEV-CEW: developed countries (12) equally weighted withOUT rebalancingΒΆ
dati_ew_dev = pd.DataFrame()
for nome in elenco_ew_dev:
print(nome, end=" - ")
df = pd.read_csv(urlo + elenco_ew_dev[nome], index_col=0)
df.index = pd.to_datetime(df.index)
df.rename(columns={df.columns[0]:nome}, inplace=True)
dati_ew_dev = pd.concat([dati_ew_dev,df], axis=1)
# dati_ew_dev.dropna(inplace=True) # non elimino qui i missing perchΓ© tanto quando farΓ² la media i paesi che non esistevano verranno automaticamente esclusi
# invece elimino i dati che non ho in quelli PRIMA
dati_ew_dev = dati_ew_dev.loc[dati.index[0]:]
display(dati_ew_dev)
dati["DEV-CEW"] = (dati_ew_dev/dati_ew_dev.iloc[0]).mean(axis=1)
(100*dati/dati.iloc[0]).plot(figsize=(20,5));
Rendimenti e volatilitΓ su piΓΉ anniΒΆ
Attenzione al EW geografico che non possiamo cosi come lo abbiamo nei "dati" perchΓ© altrimenti stiamo supponendo un ribilanciamento giornaliero che un investitore ovviamente non fa.
Per EW geografico dobbiamo ricalcolare l'indice a seconda del periodo di detenzione. Lascio comunque EW per indicare un EW geografico ribilanciato giornalmente, ma aggiungo EW nr (non ribilanciato).
VERBOSE = False
for starting_year in [2000, 2003, 2008, 2018]:
starting_date = dati.query(f"index >= '{starting_year}-01-01'").index[0].date()
descr = {}
holding_years = [1,2,3,7,12]
for anni in holding_years:
rendimenti = (( 1 + dati.loc[starting_date:].pct_change(anni*261,fill_method=None) )**(1/anni) - 1)
rendimenti["ALL-CEW"] = (( 1 + dati_ew.loc[starting_date:].pct_change(anni*261,fill_method=None) )**(1/anni) - 1).mean(axis=1).astype(float) # here there is some non-float values messing everything up
rendimenti["DEV-CEW"] = (( 1 + dati_ew_dev.loc[starting_date:].pct_change(anni*261,fill_method=None) )**(1/anni) - 1).mean(axis=1).astype(float)
if VERBOSE:
rendimenti.dropna().plot(figsize=(15,6), title=f"Rendimenti {anni} anni", legend=True, grid=True)
plt.show()
descr[anni] = (100*rendimenti).describe(include="all")
if VERBOSE:
display(descr[anni])
if anni==7 and starting_year==2000:
print("Table for Comparison with Coletti @ https://youtu.be/bjZq9Fq5Wtg?t=1525")
print("Note: sligtly different because the final date is different than the video, but `min` and `max` should be EXACTLY the same unless some very outlier pariod happened in the meanwhile")
display(descr[anni])
descr_df = pd.DataFrame()
for y in descr:
df = (
descr[y]
.reset_index()
.melt(id_vars='index', var_name='col', value_name='value')
.assign(years=y)
.query("index.isin(['mean', 'std'])")
.pivot(index=["col", "years"], columns="index", values="value").reset_index()
)
descr_df = pd.concat([descr_df, df], axis=0)
descr_df.dropna(inplace=True) # Note that in certain case the total reference period is < holding period: discard it!
holding_years_notna = descr_df.years.unique().astype(str).tolist()
if VERBOSE:
display(descr_df)
fig = px.line(
descr_df.astype({"years": str}),
x="std",
y="mean",
color="col",
markers=True,
hover_data={"mean": ":.2f", "std": ":.2f", "years": True},
symbol="years",
).update_layout(
width=800,
height=400,
title=f"Backtesting <b><span style='color:red'>{starting_date}</span></b> - {dati.index[-1].date()} for holding periods of {','.join(holding_years_notna)} years (EUR)",
xaxis_title="Standard Dev (%)",
yaxis_title="Average Yield (%)",
legend_title="Holding period (years)",
yaxis_range=[5, 16],
xaxis_range=[0, 20],
)
fig.show()
ConclusionΒΆ
- The thesis of Coletti that MCW and ECW have lower volatily (stdev) and sometimes even optimal pareto return/volatility is particularly biased in the choice of 3-7 years of holding and data from 2000-12-29
- He anyways has a point that standard volatility computed as montly standard detivation is misleading for the investor who has a clear minimal horizon UNLESS we factor in the risk of urgent unexpected liquidation