Efficient Frontier ETF Portfolio (Teil 3 von 3)

Published Dec. 9, 2019, 9:21 a.m. by finsteininvest

In Teil 2 habe ich die Performance einzelner ETFs betrachtet. In diesem finalen Teil der Serie möchte ich nun analysieren, ob durch eine Gewichtung der ETFs sich ein Portfolio aufbauen lässt, dass weniger Risiko bei gleichem Ertrag bringt, als ein gleichgewichtetes Portfolio.

Teil 1 gibt es übrigens hier.

Mit dem Programm eff_front_etf.py führe ich eine Monta Carlo Simulation durch. Es geht hierbei einfach darum die Gewichte, also wie viel % meiner Anlagesumme soll in welches ETF investiert werden, zu variieren und das Ergebnis anzuschauen. Drei Paramter werden ermittelt: Ertrag, Volatilität und Sharpe Ratio. Das ganze grafisch dargestellt ähnelt irgendwie einer Kugel. Der linke oberer Grenzvelauf stellt den "Efficient Frontier" dar. Und wir suchen den Punkt mit der maximalen Rendite bei minimaler Volatilität und maximalem Sharpe. Das ist der rote Punkt.

Die Simulation von 50.000 Durchläufen gibt folgende Verteilung der einzelnen Fonds:

|--------------|------------|-------------------------------|
| ISIN | Gewichtung | Gewichtung gerundet |
|--------------|------------|-------------------------------|
| IE00B6YX5D40 | 0.57749609 | 0.58 |
| IE00B9CQXS71 | 0.00848658 | |
| DE000A0D8Q49 | 0.00469224 | |
| IE00B9KNR336 | 0.40932508 | 0.42 (etwas stärker gerundet) |
|--------------|------------|-------------------------------|

Hier gehts zu den Profilen der ETFs bei ExtraETF:

Und hier die Monate in denen ausgeschüttet wird:

|              |                                                       | Ausschüttung Monat                               |
| ISIN | Name | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------------|-------------------------------------------------------|---|---|---|---|---|---|---|---|---|----|----|----|
| IE00B6YX5D40 | SPDR® S&P US Dividend Aristocrats UCITS ETF | | | X | | | X | | | X | | | X |
| IE00B9CQXS71 | SPDR® S&P Global Dividend Aristocrats UCITS ETF | | X | | | X | | | X | | | X | |
| DE000A0D8Q49 | iShares Dow Jones U.S. Select Dividend UCITS ETF (DE) | X | | | X | | | X | | | X | | |
| IE00B9KNR336 | SPDR® S&P Pan Asia Dividend Aristocrats UCITS ETF | | X | | | | | | X | | | | |
|--------------|-------------------------------------------------------|---|---|---|---|---|---|---|---|---|----|----|----|

Dieses Ergebnis habe ich nicht erwartet. Nur zwei Fonds zu kaufen bringt nicht die erhofften monatlichen Ausschüttungen.

Und schauen wir uns die Statistik an. Da haben wir zwar eine bessere Performance, allerdings zu Lasten einer höheren Volatilität:

|-------------------|-----------|-------------|
| Metric | Gewichtet | Ungewichtet |
|-------------------|-----------|-------------|
| Cumulative Return | 108.79% | 99.82% |
| CAGR% | 13.41% | 12.56% |
| Sharpe | 0.92 | 0.88 |
| Sortino | 1.31 | 1.25 |
| Max Drawdown | -20.06% | -18.85% |
| Longest DD Days | 451 | 445 |
| Volatility (ann.) | 10.04% | 9.88% |
|-------------------|-----------|-------------|

Den vollen Bericht gibt es hier.

Wie sieht es aus, wenn ich 4000€ investieren möchte?

|------------------|------------|---------|------------|--------|-----------|----------|
| Investmentbetrag | | | 4.000,00 € | | | |
|------------------|------------|---------|------------|--------|-----------|----------|
| ISIN | Kurs | Gewicht | Anzahl | Aussch | A.Rendite | Erw.Div. |
|------------------|------------|---------|------------|--------|-----------|----------|
| Gewichtet | | | | | | |
| IE00B6YX5D40 | 53,33 | 0,58 | 44 | 1,04 | 1,95 % | 45,76 € |
| IE00B9CQXS71 | 31,44 | 0 | 0 | 1,12 | 3,56 % | 0,00 € |
| DE000A0D8Q49 | 66,48 | 0 | 0 | 1,57 | 2,36 % | 0,00 € |
| IE00B9KNR336 | 44,5 | 0,42 | 38 | 1,21 | 2,72 % | 45,98 € |
| | Prüfsumme: | 1 | | | Summe: | 91,74 € |
| | | | | | | |
| Ungewichtet | | | | | | |
| IE00B6YX5D40 | 53,33 | 0,25 | 19 | 1,04 | 1,95 % | 19,76 € |
| IE00B9CQXS71 | 31,44 | 0,25 | 32 | 1,12 | 3,56 % | 35,84 € |
| DE000A0D8Q49 | 66,48 | 0,25 | 15 | 1,57 | 2,36 % | 23,55 € |
| IE00B9KNR336 | 44,50 | 0,25 | 22 | 1,21 | 2,72 % | 26,62 € |
| | Prüfsumme: | 1 | | | Summe: | 105,77 € |
|------------------|------------|---------|------------|--------|-----------|----------|

Bei den Dividendeneinnahmen gewinnt das ungewichtete Portfolio.

Fazit: Unerwartetes Ergebnis. Ich werde ein ungewichtetes Portfolio aufbauen.

Update 11.10.2019:
Nach der Veröffentlichung von diesem Artikel, habe ich mir einige Gedanken gemacht.
Warum favorisiert die Monte Carlo Simulation den Kauf von lediglich zwei ETFs?
Ich glaube, das hat damit zu tun, dass die vier ETFs von ihrem Profil her relativ homogen sind. Sprich: ähnlich Erträge, ähnliche Volatilität und ähnliches Sharpe Ratio. Da ist es schon interresant, dass die Simulation eine Kombination von 2 ETFs ermittelt, die als Portfolio einen besseren Sharpe und weniger Volatilität aufweisen. Auch die Kombination aus gleichgewichteten ETFs weist bessere Kennzahlen auf, als die ETFs einzeln betrachtet.

Programm für die Monte Carlos Simulation, Performanceberechnung und grafischer Plot.

'''
eff_front_etf.py

Programm, um ein efficient frontier portfolio
aus ETFs zu ermitteln.

Dezember 2019
'''

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from datetime import datetime
import pickle
import progress
import quantstats as qs

ETFs = ['IE00B6YX5D40', 'IE00B9CQXS71', 'DE000A0D8Q49', 'IE00B9KNR336']
erster_tag = datetime.strptime('2014-01-01', '%Y-%m-%d')
kurse_datum = [None]*4
kurse_wert = [None]*4

for num, ETF in enumerate(ETFs):
kurse_datum[num] = []
kurse_wert[num] = []
kurse_raw_list = pickle.load(open( f'{ETF}.p', "rb" ))
for eintrag in kurse_raw_list:
datum, wert = eintrag.items()
datum_tag, datum_wert = datum
close_tag, close_wert = wert
# Werte mit Datum extrahiert,
# Jetzt Datum in ein Python Datum umwandeln.
dt_datum_wert = datetime.strptime(datum_wert, '%Y-%m-%d')
# Nur die Kurse nach dem Starttag einlesen.
if dt_datum_wert >= erster_tag:
kurse_datum[num].append(dt_datum_wert)
kurse_wert[num].append(float(close_wert))

# Für weitere Umwandlungen werden die ganzen Listen im ein DataFrame umgewandelt.
df_werte = pd.DataFrame(list(zip(kurse_datum[0], kurse_wert[0],kurse_wert[1],kurse_wert[2],kurse_wert[3])), columns = ['Datum', f'{ETFs[0]}', f'{ETFs[1]}', f'{ETFs[2]}', f'{ETFs[3]}'])
df_werte = df_werte.set_index('Datum')

# Block auskommentieren, falls Diagramm gewünscht wird,
# aus dem die Performance der einzelnen Fonds zueinander
# ersichtlich wird.
'''
# Kummulierte Änderungen ermitteln
for num, ETF in enumerate(ETFs):
df_werte[f'pct_{ETFs[num]}'] = (df_werte[ETFs[num]]/df_werte[ETFs[num]].iloc[0])-1
df_werte = df_werte.drop(columns = [f'{ETFs[0]}', f'{ETFs[1]}', f'{ETFs[2]}', f'{ETFs[3]}'])
for etf in df_werte.columns.values:
plt.plot(df_werte.index, df_werte[etf], lw=3, alpha=0.8,label=etf)
plt.legend(loc='upper left', fontsize=12)
plt.ylabel('Absolute Veränderung in %')
plt.show()
'''

# Code copied from here: https://towardsdatascience.com/python-markowitz-optimization-b5e1623060f5

log_ret = np.log(df_werte/df_werte.shift(1))
np.random.seed(42)
num_ports = 50000
all_weights = np.zeros((num_ports, len(log_ret.columns)))
ret_arr = np.zeros(num_ports)
vol_arr = np.zeros(num_ports)
sharpe_arr = np.zeros(num_ports)

for x in range(num_ports):
# Fortschritt
progress.print_progress(x, num_ports)

# Weights
weights = np.array(np.random.random(4))
weights = weights/np.sum(weights)

# Save weights
all_weights[x,:] = weights

# Expected return
ret_arr[x] = np.sum( (log_ret.mean() * weights * 252))

# Expected volatility
vol_arr[x] = np.sqrt(np.dot(weights.T, np.dot(log_ret.cov()*252, weights)))

# Sharpe Ratio
sharpe_arr[x] = ret_arr[x]/vol_arr[x]

print(f'Max sharpe {sharpe_arr.max()}')
print(f'Sharpe location {sharpe_arr.argmax()}')
print(f'Weights {all_weights[sharpe_arr.argmax()]}')

max_sr_ret = ret_arr[sharpe_arr.argmax()]
max_sr_vol = vol_arr[sharpe_arr.argmax()]

#plt.figure(figsize=(12,8))
my_dpi=96
plt.figure(figsize=(800/my_dpi, 600/my_dpi), dpi=my_dpi)
plt.scatter(vol_arr, ret_arr, c=sharpe_arr, cmap='viridis')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatility')
plt.ylabel('Return')
plt.scatter(max_sr_vol, max_sr_ret,c='red', s=50) # red dot
plt.show()

# End of code copied from here: https://towardsdatascience.com/python-markowitz-optimization-b5e1623060f5

# Performance gewichtet
print('Performance gewichtet')
etf_1_weight,etf_2_weight,etf_3_weight,etf_4_weight = all_weights[sharpe_arr.argmax()]
df_werte['Portfolio_Gew'] = df_werte[f'{ETFs[0]}']*etf_1_weight + df_werte[f'{ETFs[1]}']*etf_2_weight + df_werte[f'{ETFs[2]}']*etf_3_weight + df_werte[f'{ETFs[3]}']*etf_4_weight
# Aus den Kursen eine Pandas Serie erstellen
# Braucht qunatstats
df = pd.Series(df_werte['Portfolio_Gew'].values.tolist(), index = df_werte.index)
# Wir brauchen die täglichen % Änderungen
df_cum_returns_weight = df.pct_change()

# Performance nicht gewichtet
print('Performance nicht gewichtet')
df_werte['Portfolio'] = df_werte[f'{ETFs[0]}']*0.25 + df_werte[f'{ETFs[1]}']*0.25 + df_werte[f'{ETFs[2]}']*0.25 + df_werte[f'{ETFs[3]}']*0.25
# Aus den Kursen eine Pandas Serie erstellen
# Braucht qunatstats
df = pd.Series(df_werte['Portfolio'].values.tolist(), index = df_werte.index)
# Wir brauchen die täglichen % Änderungen
df_cum_returns = df.pct_change()

# Einen vollen HTML Bericht erzeugen.
datei = 'EFFETF.html'
titel = 'Optimales ETF Portfolio'
qs.reports.html(df_cum_returns_weight, df_cum_returns, output = datei, title = titel)

Programm, um den Fortschrittsbalken zu zeigen:

import sys
import os

def print_progress(iteration, total, prefix='', suffix='', decimals=1, bar_length=100):
"""
Call in a loop to create terminal progress bar

@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
bar_length - Optional : character length of bar (Int)
"""
str_format = "{0:." + str(decimals) + "f}"
percents = str_format.format(100 * (iteration / float(total)))
filled_length = int(round(bar_length * iteration / float(total)))
bar = '█' * filled_length + '-' * (bar_length - filled_length)

#sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)),
print('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix), end="\r")

if iteration == total:
sys.stdout.write('\n')
sys.stdout.flush()

def clear_screen():
osname = os.name
if name == 'posix':
os.system('clear')
elif name == 'nt' or name == 'dos':
os.system('cls')

def debug(str):
'''Kleine Funktion, um
Meldungen auszugeben.
Allerdings, nur wenn im debug modus.
'''
if DEBUG:
print(str)