7. Returnanalyse#
Zur Einschätzung der Entwicklung von Portfolien oder Aktienkursen betrachtet man nicht die Aktienkurse selbst, sondern deren Renditen bzw. Returns. Hierzu gibt es verschiedene Definitionen, die teilweise nicht einheitlich verwendet werden. Daher wollen wir Beginn die verschiedenen Renditedefinitionen nochmals aufgreifen. Zunächst betrachten wir nur einzelne Werte, egal ob Aktien oder andere Finanzprodukte und bezeichnen den Kurs zum Zeitpunkt \(t\) mit \(S_t\) und uns interessiert die Wertentwicklung der Aktie vom Anfangszeitpunkt \(a\) nach \(t\).
7.1. Rendite vs. Log-Returns#
👉 Renditen beschreiben den prozentualer Wertzuwachs von \(a\) nach \(t\):
und werden auch als diskrete oder arithmetische Returns bezeichnet. In der Finanzmathematik arbeitet man hingegen häufig mit den Log-Returns:
die teilweise auch als stetige oder geometrische Returns bezeichnet werden Wir werden gleich an einem realen Beispiel sehen, dass sich beide Renditebegriffe nur minimal unterscheiden, wobei mit
beide einfach ineinander umgerechnet werden können.
Das folgende Beispiel zeigt die Berechnung der täglichen Returns:
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import numpy as np
# Daten von Yahoo-Finance abrufen
ticker = "ALV.DE"
start_date = datetime(year = 2024, month=1, day = 1)
print("Versuche Yahoo Finance für %s:\n" % (ticker))
try:
data = yf.download(ticker, start = start_date, group_by='ticker')
except Exception as e:
print("Fehler bei Yahoo Daten Download: %s \n" % (e))
if not data.empty:
data
print(data.head())
print(data.tail())
else:
print("Probleme bey yfiancne Download von %s" % ticker)
Versuche Yahoo Finance für ALV.DE:
/tmp/ipykernel_3383060/1206153666.py:15: FutureWarning: YF.download() has changed argument auto_adjust default to True
data = yf.download(ticker, start = start_date, group_by='ticker')
[*********************100%***********************] 1 of 1 completed
Ticker ALV.DE
Price Open High Low Close Volume
Date
2024-01-02 221.062945 224.475815 220.880928 222.928650 778095
2024-01-03 224.339302 225.021876 220.061841 220.744415 686416
2024-01-04 221.244983 223.429214 221.199476 223.429214 545050
2024-01-05 222.109576 222.974167 220.198363 221.973053 634842
2024-01-08 221.927555 222.928655 220.334882 222.564621 437610
Ticker ALV.DE
Price Open High Low Close Volume
Date
2025-09-23 349.899994 350.100006 347.100006 348.200012 335118
2025-09-24 347.000000 347.600006 345.700012 346.000000 362950
2025-09-25 345.600006 350.799988 345.100006 349.299988 408063
2025-09-26 352.600006 358.799988 352.299988 358.100006 676522
2025-10-01 355.000000 358.399994 354.600006 357.899994 50790
df = data["ALV.DE"]
#Datum aus Index in Spalte holen:
# nur zur Demonstration, ist hier nicht von Belang ob Date als Index oder Spalte!
df = df.reset_index()
df2 = df[["Date", "Close"]].copy()
print(df2.head())
Price Date Close
0 2024-01-02 222.928650
1 2024-01-03 220.744415
2 2024-01-04 223.429214
3 2024-01-05 221.973053
4 2024-01-08 222.564621
Verschiedene Methoden zur Berechnung beider Renditen:
df2["Rendite"] = df2["Close"].pct_change()
df2['LogReturn'] = np.log(df2['Close']).diff() # = log(C_t) - log(C_{t-1})
#nur zum Vergleich alternative Berechnungsformeln, die natürlich identische Werte liefern!
df2['Rendite2'] = df2['Close'] / df2['Close'].shift(1) - 1
df2["LogReturn2"] = np.log(df2["Close"] / df2["Close"].shift(1))
df2["LogReturn3"] = np.log(df2['Rendite2'] +1)
print(df2.head())
Price Date Close Rendite LogReturn Rendite2 LogReturn2 \
0 2024-01-02 222.928650 NaN NaN NaN NaN
1 2024-01-03 220.744415 -0.009798 -0.009846 -0.009798 -0.009846
2 2024-01-04 223.429214 0.012162 0.012089 0.012162 0.012089
3 2024-01-05 221.973053 -0.006517 -0.006539 -0.006517 -0.006539
4 2024-01-08 222.564621 0.002665 0.002661 0.002665 0.002661
Price LogReturn3
0 NaN
1 -0.009846
2 0.012089
3 -0.006539
4 0.002661
Offensichtlich sind die Spalten Rendite und Rendite2 sowie die drei verschieden berechneten Log-Returns gleich. Mittels plot()
können wir den zeitlichen Verlauf darstellen. Da die Schlusskurse und die Renditen völlig verschiedene Wertebereiche haben, ist ein simpler Plot aller Zeitreihen nicht sinnvoll, so dass wir zuvor noch die Schluss-Kurse entfernen. Probieren Sie zum Vergleich einfach mal den folgenden Befehl:
df2.plot()
<Axes: >

#Entfernen der Closing-Kurse
df3 = df2.drop(columns="Close").dropna().copy()
#Durch Indexierung des Datums steht dieses autoamtisch auf der x-Achse, sonst der Integer-Index 0,1,2,..
df3 = df3.set_index("Date")
#plot aller Renditen
print(df3.head())
df3.plot(ylim=(-0.05,0.05))
Price Rendite LogReturn Rendite2 LogReturn2 LogReturn3
Date
2024-01-03 -0.009798 -0.009846 -0.009798 -0.009846 -0.009846
2024-01-04 0.012162 0.012089 0.012162 0.012089 0.012089
2024-01-05 -0.006517 -0.006539 -0.006517 -0.006539 -0.006539
2024-01-08 0.002665 0.002661 0.002665 0.002661 0.002661
2024-01-09 -0.004089 -0.004098 -0.004089 -0.004098 -0.004098
<Axes: xlabel='Date'>

Hinweis: die Pandas Methode pct_change()
kann auch auf den kompletten DataFrame angewendet werden, solange das Datum als Index gesetzt ist. Dann werden die prozentualen Veränderungen auch für die Open, High und Low-Kurse berechnet. Ist das Datum allerdings als Spalte gesetzt, so führt der Aufruf zu einer Fehlermeldung, da die Funktion pct_change()
nicht auf das Dates-Format angewendet werden kann. Außerdem werden die normalen Kurse überschrieben – möchte man beispielsweise die Closing-Kurse im DataFrame erhalten, sollte wie oben in df2 einfach eine neue Variable / Spalte erzeugt werden.
Hinweis: Für die Berechnung der Renditen mittels der Pandas Funktion pct_change()
ist es notwendig entweder eine konkrete Variable (Spalte) das DataFrames anzugeben, so wie das oben realisiert wurde. Ruft man mehrere Ticker gleichzeitig ab und möchte dann beispielsweise die Renditen auch gleichzeitig für alle Kurse berechnen, kann man andernfalls die Funktion pct_change()
auch auf den gesamten DateFrame anwenden. Dann sollte man allerdings darauf achten, dass das Datum als Index gesetzt[1] ist. Natürlich ist es auch möglich, die Funktion nur auf eine Teilmenge aller Spalten bzw. Variablen anzuwenden.
data_all = data.pct_change()
data_all.head()
Ticker | ALV.DE | ||||
---|---|---|---|---|---|
Price | Open | High | Low | Close | Volume |
Date | |||||
2024-01-02 | NaN | NaN | NaN | NaN | NaN |
2024-01-03 | 0.014821 | 0.002433 | -0.003708 | -0.009798 | -0.117825 |
2024-01-04 | -0.013793 | -0.007078 | 0.005170 | 0.012162 | -0.205948 |
2024-01-05 | 0.003908 | -0.002037 | -0.004526 | -0.006517 | 0.164741 |
2024-01-08 | -0.000820 | -0.000204 | 0.000620 | 0.002665 | -0.310679 |
Obiger Graph der verschieden berechneten Renditen zeigt, dass auch zwischen der klassichen Rendite als prozentuale Veränderung und den Log-Returns keine großartigen Unterschiede mit bloßen Auge erkennbar sind, denn für sehr kleine Zeitabstände (hier 1 Tag) ist der Quotient \(\frac{S_t}{S_{t-1}} \approx 1\) und somit die Unterschiede zwischen Log-Returns und Renditen nur marginal, was sich mit der Taylorapproximation 1. Ordnung der Funktion \(f(x) = \log(x) \approx x-1 \) für \(x\) nahe 1 erklären lässt. Berücksichtigt man höhere Ordnungen der Taylorapproximation \(f(x) = \log(x) = x-1 - \frac 12 (x-1)^2 + \frac13 (x-1)^3 - \frac 14 (x-1)^4 \pm \ldots\) wird deutlich, warum sich aber insbesondere für \(x\neq 1\) beide Return-Begriffe unterscheiden.
# x-Werte
x = np.linspace(0.01, 2, 400) # für f(x) = ln(x), nur positive x
# Funktionen
f = np.log(x)
t = x - 1 #Taylor-Approximation 1. Oridnung
# Plot
plt.figure(figsize=(7,5))
plt.plot(x, f, label="f(x) = ln(x)", color="blue")
plt.plot(x, t, label="t(x) = x - 1", color="red")
# Achsen und Details
plt.axhline(0, color="black", linewidth=0.8) # x-Achse
plt.axvline(0, color="black", linewidth=0.8) # y-Achse
plt.title("f(x) = ln(x) und Taylorapproximation t(x) = x - 1")
plt.xlabel("x")
plt.ylabel("y")
plt.ylim([-2,1])
plt.legend()
plt.grid(True)
plt.show()

Es ist deutlich zu erkennen, dass für x nahe 1 kaum ein Unterschied zwischen beiden Funktionen zu erkennen ist. Zusammenfassend: Log-Returns und arithmetische Renditen unterscheiden sich für Tageskurse wegen \(x= \frac{S_t}{S_{t-1}} \approx 1\)
nur minimal, da die Kursänderungen von Tag zu Tag nicht wesentlich sind. Beide Rendite-Begriffe haben aus finanzmathematischer Sicht Vor- und Nachteile, die wir später nochmals kurz aufgreifen werden. Aus praktischer Sicht macht es hingegen kaum einen Unterschied, ob wir Renditen oder Log-Returns analysieren. Betrachtet man beispielsweise das Histogramm der täglichen Returns ist optisch kein wesentlicher Unterschied zwischen Log-Returns und Renditen erkennbar. Für Tagesrenditen bzw. -Returns schreibt man häufig \(R_t\) bzw. \(r_t\), d. h. lässt den ersten Zeitindex des Anfangsstadiums einfach weg. Außerdem zeigt die Graphik, dass die rote Kurve, welche den prozentualen Zuwächsen und somit Renditen entspricht, immer oberhalb der blaue Kurve verläuft, welche die Log-Returns beschreibt.
7.2. Wertzuwachs über die Zeit: kumulierte Returns vs. Renditen#
Die Log-Returns des Zuwachses von der Anfangsinvestition bis zum Zeitpunkt \(t\) ist wegen
gerade die Summe der täglichen Log-Returns. Somit folgt dann für die Renditen eine Produkt-Struktur
also ähnlich wie beim (geometrischen) Verzinsen mit Zinseszinseffekten.
Zusammenfassend werden wir die kumulierten Tages-Returns von 0 bis zum Zeitpunkt t zur Annualisierung verwenden
bzw.
Die Funktionen cumsum()
und cumprod()
berechnen für jede Spalte die kumulierten Summen bzw. Produkte, d. h. in der ersten Zeile wird die nur das erste Element und in der zweiten Zeile die ersten beiden Werte usw. in die Summe bzw. das Produkt berücksichtigt. Somit kann die Entwicklung über die Zeit beginnend vom Startwert für beide Returnarten einach berechnet werden.
df3['LogReturns_kum']=df3['LogReturn'].cumsum()
df3['Rendite_kum']=(1+df3['Rendite']).cumprod()-1
print(df3.tail())
df3.LogReturns_kum.plot()
df3.Rendite_kum.plot()
plt.legend(['LogReturns','Rendite'])
Price Rendite LogReturn Rendite2 LogReturn2 LogReturn3 \
Date
2025-09-23 0.002014 0.002012 0.002014 0.002012 0.002012
2025-09-24 -0.006318 -0.006338 -0.006318 -0.006338 -0.006338
2025-09-25 0.009538 0.009492 0.009538 0.009492 0.009492
2025-09-26 0.025193 0.024881 0.025193 0.024881 0.024881
2025-10-01 -0.000559 -0.000559 -0.000559 -0.000559 -0.000559
Price LogReturns_kum Rendite_kum
Date
2025-09-23 0.445925 0.561935
2025-09-24 0.439587 0.552066
2025-09-25 0.449079 0.566869
2025-09-26 0.473961 0.606344
2025-10-01 0.473402 0.605446
<matplotlib.legend.Legend at 0x7fda92bf9750>

Es ist deutlich zu erkennen, dass für die frühen Zeitpunkte \(t\) sich die Log-Returns und Renditen von 0 (also Jahresanfang 2024) bis \(t\) kaum unterscheiden. Mit steigender Zeit \(t\) laufen die beiden Renditekurven allerdings auseinander, da sich für größere Zeitabstände eben beide Returndefinitionen unterscheiden. Wie erwartet liegen die (kumulierten) Renditen immer oberhalb der Log-Returns
7.3. Annualisierte Returns#
Aus Gleichung (7.3) folgt
d. h. die kumulierte Rendite von 0 nach t lässt sich basierend auf der modifizierten Stichprobe der Tages-Renditen \(y_k = R_{k-1,k}+1\) mit Hilfe des geometrischen Mittels für \(n=t\)
aller Tagesrenditen bis zum Zeitpunkt t ausdrücken. Ebenso können wir für die Log-Returns für die Stichprobe der Tages-Returns \(x_k = r_{k-1,k}\) mittels des arithmetischen Mittels \(\bar{x}=\frac 1n \sum\limits_{k=1}^n x_k\) wegen Formel (7.2) schreiben als
Außerdem gilt folgender Zusammenhang zwischen geometrischen Mittel der Renditen und arithmetischen Mittel der Log-Returns für beliebige Stichprobengröße \(n\)
Somit wird aus allen vorhanden Tages-Returns das jeweilige arithmetische und geometrische Mittel geschätzt und dann annualisiert man diese Werte entsprechend obiger Formeln, wobei man typischerweise unterstellt, dass ein Handelsjahr 252 Handelstage hat
sowie
Insbesondere für statistische Analylsen werden häufig die Maßzahlen für jährliche Renditen angegeben, um eine bessere Vergleichbarkeit zwischen verschiedenen Anlageprodukten und Anlagezeiträumen durch die entsprechende Skalierung zu erreichen.
In Abschnitt Portfolioanalyse: erwartete Portfoliorendite und Portfoliovarianz benötigen wir die Erwartungswerte, Varianzen sowie Kovarianzen zwischen den einzelnen Aktienrenditen, um die erwartete Portfoliorendite und -Varianz zu berechnen. Für die Log-Returns ist dies aufgrund der Linearität des Erwartungswertes sehr einfach. Aus diesem Grund arbeitet man in der Finanzmathematik auch häufig lieber mit den Log-Returns als den Renditen. Unterstellt man, dass die Tages-Returns der \(j\)-ten Aktie \(r_{j,(k-1,k)}\) unabhängig und identisch verteilte Zufallsvariablen (iid) mit \(\mathbb{E} r_{j,(k-1,k)} = \mu_i\) und \(\operatorname{Var} r_{j,(k-1,k)}=\sigma^2_j\) sind, folgt für die erwarteten Jahres-Log-Returns sowie deren Varianzen bzw. Volatilitäten / Standardabweichungen
Zur Annualisierung der Kovarianz der täglichen Log-Returns führen wir folgende Bezeichnung ein: \(X_k\) sei der zufällige Tages-Return der \(i\)-ten Aktie von Tag \((k-1)\)-ten Tag zum \(k\)-ten Tag und für die \(j\)-te Aktie sei \(Y_l\) der Tages-Return vom \((l-1)\)-ten Tag zum \(l\)-ten Tag, d. h.
Mit (7.2) folgt daher für die Jahres-Returns (für \(t=252\))
Unterstellen wir zusätzlich zur Standardannahme (tägliche Returns einer Aktie sind identisch und unabhängig verteilt, d.h. \(X_1, \ldots, X_t\) iid sowie \(Y_1, \ldots, Y_t\) iid), dass ausschließlich die Tages-Returns zweier verschiedener Aktien am selben Tag korrelieren, also genauer
erhalten wir für die Kovarianz der Jahres-Returns zwischen der \(i\)-ten und \(j\)-ten Aktie wegen
dass die Kovarianzen der Returns von 0 bis t einfach mit der Zeit skalieren
und somit die annualisierte Kovarianzmatrix aller Returns unter der Annahme dass ein Jahr 252 Handelstage besitzt
wobei \(\Sigma_\text{daily}\) die Kovarianzmatrix der Tages-Returns bezeichnet.
Die Annualisierung für die prozentualen Zuwächse ist wegen der Nichtlinearität in (7.4) bzw. (7.2) komplizierter, aber unter den obigen Annahmen der Unabhängigkeit der Tages-Renditen verschiedener Tage faktorisiert der Erwartungswert des Produktes. Analog zu den Log-Returns führen wir für die zufälligen Renditen folgende Bezeichnungen ein
wobei analog zu den Log-Returns \(\mathbb{E} R_{i,(k-1,k)} = \tilde{\mu}_i\) und \(\operatorname{Var} R_{i,(k-1,k)} = \tilde{\sigma}^2_i\) sowie
Aus Formel (7.3) bzw. (7.4) folgt dann für die erwartete Jahres-Rendite
zusammengefasst also
und die Varianz skaliert wegen
aus den Tageswerten
Insbesondere die Skalierung der Varianz für die annualisierten Renditen ist demnach also deutlich komplexer und insbesondere auch nichtlinear in der Zeit \(t\). Für die Kovarianzen der Jahres-Renditen zwischen der \(i\)-ten und \(j\)-ten Aktie ergibt sich bei analogen Annahmen zu den Log-Returns
denn
wobei die letzte Gleichung aus
folgt. Prinzipiell können wir also aus mit Formel (7.6) von den täglichen Erwartungswertvektor bzw. Kovarianzmatrix der Renditen übergehen zu den annualisierten Werten, allerdings nicht so einfach wie für die Log-Returns.
7.4. Statistische Analyse der Returns#
Wie oben gesehen sind die Unterschiede zwischen täglichen Log-Returns und Renditen nicht wesentlich. So sehen die Histogrammen, die die Verteilung der täglichen Wertzuwächse wiedergeben sehr ähnlich aus, wie die folgenden beiden Graphiken zeigen. Der folgende Code erzeugt nicht nur ein klassisches Histogramm, sondern fügt noch den Standard-Kerndichteschätzer[2], was man einfach als empirische Dichtefunktion interpretieren kann, sondern auch noch zum Vergleich die angepasste Normalverteilungsdichte[3].
from scipy.stats import norm
#Parameter der NV aus Daten schaetzen
mu_log, std_log = norm.fit (df3.LogReturn)
#Alternative mit selben Schaetzwerten
#mu2 = np.mean(df3['LogReturn'])
#std2 = np.std(df3['LogReturn'])
#print("Schätzer mu=%f vs mu2 =%f" % (mu, mean))
#print("Schätzer std=%f vs std2 =%f" % (std, std2))
# x Werte fuer Dichteplot der Normalverteilung
x = np.linspace(min(df3['LogReturn']), max(df3['LogReturn']), 1000)
#Histogramm mit kernel
sns.histplot(df3['LogReturn'], stat = 'density', kde=True)
#Dichte = pdf mit gefitteten Parametern
y = norm.pdf(x, mu_log, std_log)
#hinzufügen der Dichte zum Histogramm:
plt.plot(x, y, color='red')
#Legende hinzufügen
plt.legend(['Kerndichteschätzer','Normaldichte'])
<matplotlib.legend.Legend at 0x7fda974a4910>

#Analoges Vorgehen für Renditen
mu, std = norm.fit (df3.Rendite)
x = np.linspace(min(df3['Rendite']), max(df3['Rendite']), 1000)
sns.histplot(df3['Rendite'], stat = 'density', kde=True)
y = norm.pdf(x, mu, std)
plt.plot(x, y, color='red')
plt.legend(['Kerndichteschätzer','Normaldichte'])
<matplotlib.legend.Legend at 0x7fda91a31790>

Der Kerndichteschätzer zeigt eine typische glockenförmige Verteilung. Allerdings zeigt der Vergleich zwischen Kerndichteschätzer und Normalverteilungsdichte eine recht deutlichte Abweichung zur Normalverteilung. In den Rändern der empirischen Verteilung liegen deutlich mehr Werte als bei der Normalverteilung üblich. Insbesondere die linke Seite, die starke Verluste repräsentiert, ist stärker ausgeprägt und dies spricht für eine leptokurtsiche Verteilung. Mit Hilfe der 3. und 4. Momente werden wir dies auch mit Hilfe statistischer Maßzahlen in höhere Momente: Schiefe und Kurtosis ausdrücken.
print("mittlere Tages-Rendite: %f Tages-LogReturns: %f" % (mu, mu_log))
print("Standardabweichung Tages-Rendite: %f Tages-LogReturns: %f" % (std, std_log))
mittlere Tages-Rendite: 0.001133 Tages-LogReturns: 0.001069
Standardabweichung Tages-Rendite: 0.011271 Tages-LogReturns: 0.011297
Oben haben wir bereits mittleren Tages-Returns und die Standardabweichungen berechnet. Allerdings kann man dies auch gleich spaltenweise für den gesamten DataFrame. Zu beachten gilt hier natürlich, ob die Maßzahl tatsächlich auch sinnvoll für jede Variable / Spalte ist. Die Mittelung der durchschnittlich kumilierten Returns ist beipspielsweise nicht sinnvoll, da für jede Zeile verschiedene Zeitpunkte betrachtet werden und somit keine stationäre Zeitreihe mehr vorliegt. Daher sollten die letzten beiden Spalten bei der Interpretation ausgelassen (oder noch besser ausgeblendet) werden, wie wir im Folgenden noch demonstrieren.
df3.mean()
Price
Rendite 0.001133
LogReturn 0.001069
Rendite2 0.001133
LogReturn2 0.001069
LogReturn3 0.001069
LogReturns_kum 0.249533
Rendite_kum 0.299482
dtype: float64
df3.std()
Price
Rendite 0.011284
LogReturn 0.011309
Rendite2 0.011284
LogReturn2 0.011309
LogReturn3 0.011309
LogReturns_kum 0.157830
Rendite_kum 0.205357
dtype: float64
Die describe()
Funktion liefert eine Übersicht der wichtigsten Statistik-Maßzahlen
df3.describe()
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 | LogReturns_kum | Rendite_kum |
---|---|---|---|---|---|---|---|
count | 443.000000 | 443.000000 | 443.000000 | 443.000000 | 443.000000 | 443.000000 | 443.000000 |
mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 | 0.249533 | 0.299482 |
std | 0.011284 | 0.011309 | 0.011284 | 0.011309 | 0.011309 | 0.157830 | 0.205357 |
min | -0.060486 | -0.062393 | -0.060486 | -0.062393 | -0.062393 | -0.021875 | -0.021637 |
25% | -0.004993 | -0.005006 | -0.004993 | -0.005006 | -0.005006 | 0.111597 | 0.118062 |
50% | 0.001625 | 0.001623 | 0.001625 | 0.001623 | 0.001623 | 0.236534 | 0.266851 |
75% | 0.007328 | 0.007301 | 0.007328 | 0.007301 | 0.007301 | 0.422829 | 0.526273 |
max | 0.043782 | 0.042850 | 0.043782 | 0.042850 | 0.042850 | 0.526719 | 0.693367 |
Mittels apply()
können auch einzelne oder mehrere Funktionen auf die Spalten (axis = 0) bzw. Zeilen (axis=1) angewendet werden
df3.apply([np.mean, np.std], axis = 0)
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 | LogReturns_kum | Rendite_kum |
---|---|---|---|---|---|---|---|
mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 | 0.249533 | 0.299482 |
std | 0.011271 | 0.011297 | 0.011271 | 0.011297 | 0.011297 | 0.157652 | 0.205125 |
Natürlich können auch eigene Funktionen angewendet werden. Der folgende Code zeigt beispielhaft die selbstständige Berechnung der ersten beiden empirischen Momente:
def my_mean(x):
return np.sum(x) / x.size
def my_var(x, correction = True):
n = x.size
m = my_mean(x)
ss = ((x-m)**2).sum()
df = n #biased variance estimator!
if correction:
#Bessel correction gives unbiased variance estimate
df = df - 1
return ss/df
def my_var2(x):
n = x.size
m = my_mean(x)
ss = (x**2).sum() - n*m**2
return ss/(n-1)
def my_var_biased(x):
return my_var(x, correction=False)
def my_sd(x):
return np.sqrt(my_var(x))
df3.apply([my_mean, my_var, my_var_biased, my_sd], axis = 0, correction = True)
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 | LogReturns_kum | Rendite_kum |
---|---|---|---|---|---|---|---|
my_mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 | 0.249533 | 0.299482 |
my_var | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 | 0.024910 | 0.042172 |
my_var_biased | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 | 0.024854 | 0.042076 |
my_sd | 0.011284 | 0.011309 | 0.011284 | 0.011309 | 0.011309 | 0.157830 | 0.205357 |
Die Berechnung der statistischen Kennzahlen für die kumulierten Returns (letzten beiden Spalten) macht wie oben bereits erwähnt keinen Sinn, daher schließen wir sie einfach aus der Auswertung aus.
df3.drop(columns=["LogReturns_kum","Rendite_kum"]).apply([my_mean, my_var, my_var_biased, my_sd], axis = 0, correction = True)
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 |
---|---|---|---|---|---|
my_mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 |
my_var | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 |
my_var_biased | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 |
my_sd | 0.011284 | 0.011309 | 0.011284 | 0.011309 | 0.011309 |
Alternativ kann man natürlich einfach die Variablen angeben, die man auswerten möchte: hier bietet sich neben der Funktion drop()
auch die Pandas Funktion filter()
an, bei der man beispielsweise mittels sogenannter Regular Expressions (siehe auch Regex Cheat Sheet) sehr vielfältige Bedingungen formulieren kann, siehe
Pandas Docu.
Im folgenden Code wollen wir alle Variablen die mit ‘Log’ beginnen (^Log
) aber am Ende nicht auf ‘_kum’ enden (negative lookahead: (?!.*_kum$)
) auswerten:
df3.filter(regex=r"^Log(?!.*_kum$)").apply([my_mean, my_var, my_var_biased, my_sd], axis = 0, correction = True)
Price | LogReturn | LogReturn2 | LogReturn3 |
---|---|---|---|
my_mean | 0.001069 | 0.001069 | 0.001069 |
my_var | 0.000128 | 0.000128 | 0.000128 |
my_var_biased | 0.000128 | 0.000128 | 0.000128 |
my_sd | 0.011309 | 0.011309 | 0.011309 |
Hinweis: man kann das beliebig individualisieren. In manchen Auswertungen möchte man beispielsweise nur Variablen mit numerischen Werten auswerten, dann ist die Funktion select_dtypes()
für die Auswahl bestimmer Datentypen hilfreich.
Ebenso kann die Funktion agg()
wie die Funktion apply()
angewendet werden:
df3.drop(columns=["LogReturns_kum","Rendite_kum"]).agg([my_mean, np.mean, my_var, my_var2, my_var_biased, my_sd, np.std])
/tmp/ipykernel_3383060/1356242698.py:1: FutureWarning: The provided callable <function mean at 0x7fdad00c84a0> is currently using Series.mean. In a future version of pandas, the provided callable will be used directly. To keep current behavior pass the string "mean" instead.
df3.drop(columns=["LogReturns_kum","Rendite_kum"]).agg([my_mean, np.mean, my_var, my_var2, my_var_biased, my_sd, np.std])
/tmp/ipykernel_3383060/1356242698.py:1: FutureWarning: The provided callable <function std at 0x7fdad00c85e0> is currently using Series.std. In a future version of pandas, the provided callable will be used directly. To keep current behavior pass the string "std" instead.
df3.drop(columns=["LogReturns_kum","Rendite_kum"]).agg([my_mean, np.mean, my_var, my_var2, my_var_biased, my_sd, np.std])
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 |
---|---|---|---|---|---|
my_mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 |
mean | 0.001133 | 0.001069 | 0.001133 | 0.001069 | 0.001069 |
my_var | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 |
my_var2 | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 |
my_var_biased | 0.000127 | 0.000128 | 0.000127 | 0.000128 | 0.000128 |
my_sd | 0.011284 | 0.011309 | 0.011284 | 0.011309 | 0.011309 |
std | 0.011284 | 0.011309 | 0.011284 | 0.011309 | 0.011309 |
7.4.1. höhere Momente: Schiefe und Kurtosis#
Allgemein kann die Verteilung einer Stichprobe mittels der empirischen zentralen bzw. allgemeinen Momente
beschrieben werden. Während das 1. Moment \(M_1 = \overline x\) den Mittelwert der Verteilung beschreibt, wird die Schwankung mittels des 2. Moments beschrieben und entspricht dem unkorrigierten Varianzschätzer
Die sogenannte Z-Transformation einer Stichprobe \(x_1, x_2, \ldots, x_n\) ist eine Standardisierung[4] der Werte \(z_i = \frac{x_i - M_1}{\sqrt{M_2}} = \frac{x_i - \overline x}{\tilde s}\), so dass diese Stichprobe wegen \(M_1(z) = \frac1n \sum\limits_{i=1}^n z_i = 0\) um die Null schwankt mit Varianz
def my_central_moments(x,k):
n = x.size
m = x.sum()/n
return ((x-m)**k).sum()/n
#2. Moment = Varianz
s2 = my_var(df3['LogReturn'])
s2_1 = my_central_moments(df3['LogReturn'], 2)
s2_2 = my_var_biased(df3['LogReturn'])
print("unbiased var = %f, 2. zentrales Moment = %f, biased var = %f" % (s2,s2_1, s2_2))
unbiased var = 0.000128, 2. zentrales Moment = 0.000128, biased var = 0.000128
Mittels dem 3. Moment wird die (empirische) Schiefe einer Verteilung berechnet
Für symmetrische Verteilungen verschwindet die Schiefe (\(\bar{x} \approx x_{med} \approx x_{mod}\), \(g \approx 0\)), während für linksschiefe (rechtssteil) Verteilungen der Wert negativ wird (\(\bar{x} < x_{med} < x_{mod}\), \(g<0\)) und für rechtsschiefe (linkssteil) Verteilungen (\(\bar{x} > x_{med} > x_{mod}\), \(g>0\)) sich ein postiver Wert ergibt.
def schiefe(x):
m = x.sum()
s = np.sqrt(my_central_moments(x,2))
#s = x.std(ddof=0) # Populations-Std für Normierung
#n = x.size
z = (x-m)/s
return (my_central_moments(z,3))
#3. Moment / Schiefe
m3 = my_central_moments(df3['LogReturn'], 3)
g = schiefe(df3['LogReturn'])
print("Schiefe: g = %f basierend auf 3. zentralen Moment = %f" % (g, m3))
Schiefe: g = -0.632949 basierend auf 3. zentralen Moment = -0.000001
Mittels 4. Moment misst man die Wölbung bzw. Krümmung einer Verteilung
wobei die Verschiebung um 3 für die bessere Interpretierbarkeit bzw. dem Vergleich zur Normalverteilung dient, denn diese Kennzahl vergleicht wie stark der zentrale Bereich und folglich auch wie schwach die ‘tails’ im Vergleich zur Normalverteilung besetzt sind. Werte von \(\gamma \approx 0\) sind typisch für eine Normalverteilung, während positive Werte für eine spitzere Verteilung (\(\gamma > 0\)) mit mehr Maße in den Tails und negative Werte für eine breitere Verteilung (\(\gamma < 0\)) sprechen.
def woelbung(x):
m = x.sum()
s = np.sqrt(my_central_moments(x,2))
#s = x.std(ddof=0) # Populations-Std für Normierung
#n = x.size
z = (x-m)/s
return (my_central_moments(z,4) - 3)
#4. Moment / Krümmung
m4 = my_central_moments(df3['LogReturn'], 4)
k = woelbung(df3['LogReturn'])
print("Kurtosis: gamma = %f basierend auf 4. zentralen Moment = %f" % (k, m4))
Kurtosis: gamma = 3.856050 basierend auf 4. zentralen Moment = 0.000000
df3.drop(columns=["LogReturns_kum","Rendite_kum"]).agg([schiefe, woelbung])
Price | Rendite | LogReturn | Rendite2 | LogReturn2 | LogReturn3 |
---|---|---|---|---|---|
schiefe | -0.541581 | -0.632949 | -0.541581 | -0.632949 | -0.632949 |
woelbung | 3.601680 | 3.856050 | 3.601680 | 3.856050 | 3.856050 |
Natürlich gibt es auch enstprechende Pakete, um diese Kennzahlen einfach zu berechnen. Hier nutzen wir das Sub-Package scipy.stats
from scipy.stats import skew, kurtosis, moment
#g = schiefe(df3['LogReturn'])
#k = woelbung(df3['LogReturn'])
#Package Version:
g2 = skew(df3['LogReturn'])
k2 = kurtosis(df3['LogReturn'])
m3_sci = moment(df3['LogReturn'], order=3)
print("Schiefe: g = %f, scipy.stats.skew = %f" % (g, g2 ))
print("3. Moment: m3 = %f, scipy.stats.moment = %f" % (m3, m3_sci))
print("Kurtosis: k = %f, Scipy.stats.kurtosis = %f" % (k, k2))
#kompletter DataFrame
print("Skew: %s" % df3.skew())
print("Kurtosis: %s" % df3.kurtosis())
Schiefe: g = -0.632949, scipy.stats.skew = -0.632949
3. Moment: m3 = -0.000001, scipy.stats.moment = -0.000001
Kurtosis: k = 3.856050, Scipy.stats.kurtosis = 3.856050
Skew: Price
Rendite -0.543423
LogReturn -0.635101
Rendite2 -0.543423
LogReturn2 -0.635101
LogReturn3 -0.635101
LogReturns_kum 0.060312
Rendite_kum 0.215481
dtype: float64
Kurtosis: Price
Rendite 3.656331
LogReturn 3.913595
Rendite2 3.656331
LogReturn2 3.913595
LogReturn3 3.913595
LogReturns_kum -1.320794
Rendite_kum -1.326651
dtype: float64
Wir haben hier ausschließlich Tages-Returns ausgewertet, allerdings haben wir in Annualisierte Returns gesehen, wie wir diese zu Jahres-Returns skalieren können. Insbesondere Schiefe und Kurtosis sind im Gegensatz zu den zentrierten 3. bzw. 4. Momenten aber skalierungsfrei, da sich die Skalierungsfaktoren bei der Normierung herauskürzen.
7.4.2. Test auf Normalverteilung#
to do
graphische Analyse mit Histogramm und qq-plots
Shapiro Wilk, KS-Test, Liliefors Korrektur, usw.
from scipy import stats
test1 = stats.shapiro(df3['LogReturn'])
print(test1)
test2 = stats.normaltest(df3['LogReturn'])
print(test2)
ShapiroResult(statistic=0.9541712001980666, pvalue=1.700962636691019e-10)
NormaltestResult(statistic=71.70206281258255, pvalue=2.692122945992413e-16)
to do: Auswertung… alle p-values sehr klein (<0.05) und somit signifikanter Nachweis, dass Log-Returns nicht normalverteilt…
7.4.3. Abhängigkeiten zwischen Aktien-Returns: Kovarianz und Korrelation#
Zur statistischen Auswertung zählt natürlich auch die Analyse der Abhängigkeiten zwischen verschiedenen Aktien. Im folgenden Abschnitt Portfolio-Returns werden wir aus verschiedenen Aktien ein Portfolio zusammenstellen und desen Entwicklung näher untersuchen. Bevor wir Portfolio-Returns studieren, untersuchen wir noch die Abhängigkeiten zwischen den einzelnen Aktien-Returns. Der Übersichtlichkeit wählen wir hier 4 Aktien und laden deren Kurse bei YahooFinance herunter:
tickers = ["AAPL", "MSFT", "GOOGL", "TSLA"]
# Kurse herunterladen (letztes Jahr)
portfolio_data = yf.download(tickers, period="5y")
#print(data.head())
#wollen nur Schlusskurse untersuchen
close_df = portfolio_data["Close"].copy()
print(close_df.head())
close_df.plot()
/tmp/ipykernel_3383060/3752665698.py:4: FutureWarning: YF.download() has changed argument auto_adjust default to True
portfolio_data = yf.download(tickers, period="5y")
[ 0% ]
[**********************50% ] 2 of 4 completed
[**********************75%*********** ] 3 of 4 completed
[*********************100%***********************] 4 of 4 completed
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 113.603722 73.885399 203.712036 149.386673
2020-10-02 109.936546 72.281471 197.700226 138.363327
2020-10-05 113.321632 73.633644 201.717697 141.893326
2020-10-06 110.072754 72.054039 197.431763 137.993332
2020-10-07 111.940376 72.457253 201.190353 141.766663
<Axes: xlabel='Date'>

Teilweise verlaufen alle 4 Kurse sehr ähnlich, teilweise aber auch individuell. Sinnvoll ist daher der Übergang zu den Renditen und die Analyse derer Abhängigkeiten.
renditen = close_df.pct_change()
print(renditen.head())
renditen.plot(alpha = 0.7)
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 NaN NaN NaN NaN
2020-10-02 -0.032280 -0.021708 -0.029511 -0.073791
2020-10-05 0.030791 0.018707 0.020321 0.025513
2020-10-06 -0.028670 -0.021452 -0.021247 -0.027485
2020-10-07 0.016967 0.005596 0.019037 0.027344
<Axes: xlabel='Date'>

Während oben bereits der zeitliche Verlauf unterschiedliche Zusammenhänge verdeutlicht, ist die Darstellung in Scatterplots übersichtlicher:
pd.plotting.scatter_matrix(renditen, figsize=(6, 6), diagonal="hist")
plt.show()

sns.pairplot(renditen)
plt.show()
/HOME1/users/personal/dana/miniconda3/envs/finance/lib/python3.11/site-packages/seaborn/axisgrid.py:123: UserWarning: The figure layout has changed to tight
self._figure.tight_layout(*args, **kwargs)

Natürlich können auch nur einzelne Paare beispielsweise mittels der Seaborn Funktionen scatterplot()
oder relplot()
dargestellt werden. Auf den ersten Blick unterscheiden sich die Funktionen nicht, allerdings liefert die zweite Funktionen flexiblere Anpassungsmöglichkeiten[5].
sns.scatterplot(data = renditen, x='AAPL', y='TSLA')
<Axes: xlabel='AAPL', ylabel='TSLA'>

sns.relplot(data = renditen, x='AAPL', y='TSLA', kind = 'scatter')
/HOME1/users/personal/dana/miniconda3/envs/finance/lib/python3.11/site-packages/seaborn/axisgrid.py:123: UserWarning: The figure layout has changed to tight
self._figure.tight_layout(*args, **kwargs)
<seaborn.axisgrid.FacetGrid at 0x7fda80e47790>

Alle Scatter-Plots zeigen einen mehr oder weniger starken positiven Zusammenhang zwischen den Renditen zweier Aktien. Steigt die Rendite der einen Aktie, dann steigt üblicherweise auch die der anderen Aktie und fällt die eine Rendite, dann auch die andere. Die einfachste statistische Maßzahl zur Erfassung des Zusammenhangs sind die (empirische) Kovarianz und Korrelation[6], die allerdings nur den linearen Zusammenhang erfassen. Die (unverzerrte) empirische Kovarianz zweier Stichproben \((x,y)=((x_1,y_1), \ldots, (x_n,y_n))\) wird mittels
berechnet. Die Korrelation entsteht durch Normierung und ist einfacher zu interpretieren
Pandas hat die Berechnung der empirischen Kovarianz- und Korrelation bereits implementiert und fasst alle paarweisen Kombinationen in der Kovarianz- bzw. Korrelationsmatrix zusammen. Auf der Diagonalen der Kovarianzmatrix stehen also die Varianzen.
kov_matrix = renditen.cov()
print(kov_matrix)
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.000326 0.000205 0.000192 0.000342
GOOGL 0.000205 0.000383 0.000215 0.000300
MSFT 0.000192 0.000215 0.000269 0.000264
TSLA 0.000342 0.000300 0.000264 0.001488
korr_matrix = renditen.corr()
print(korr_matrix)
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 1.000000 0.580619 0.646637 0.490582
GOOGL 0.580619 1.000000 0.669879 0.397976
MSFT 0.646637 0.669879 1.000000 0.416989
TSLA 0.490582 0.397976 0.416989 1.000000
Zum Abschluss zeichnen wir noch die Regressionsgerade in den Scatterplot ein, um den linearen Zusammenhang zu veranschaulichen. Mit Regressionsmodellen beschäftigen wir und noch ausführlich in Abschnitt Regressionsmodelle (to do)
sns.regplot(data=renditen, x="AAPL", y="MSFT", ci = None)
<Axes: xlabel='AAPL', ylabel='MSFT'>

👉Der Zusammenhang der täglichen Log-Returns ist völlig analog zu analysieren und sollte zu Übungszwecken selbstständig durchgeführt werden.
7.4.4. to do: Vorsicht bei der Skalierung der ‘zeitlineare’ Log-Returns um von Tages-Returns zu Jahres-Returns überzugehen#
bisher: annualisiere Erwartungswert und Kovarianz aus Daily Maßzahlen
einfacher Vergleich der skalierten Log-Returns zeigt es ist falsch: annualisiere Returns durch Zeitskalierung und berechne Maßzahlen, Erwartungswerte sind identisch aber Kovarianz / Varianz wird maßlos überschätzt! Grund für iid Zufallsvariablen \(X_1\) ist \(X_1 + X_2 \neq 2 X_1\) denn Varianz ist deutlich höher!!!
siehe Skript Alois re-skalierung!
Wir haben bei der Analyse der Tages-Returns gesehen, dass die Unterscheidung zwischen Log-Returns (zeitstetige Returns) und Renditen (arithmetische Returns, zweitdiskrete Returns) nur unwesentliche Unterschiede aufzeigen, da für Tages-Kurse beide Return-Begriffe zu verschiedenen, aber sehr ähnlichen Werten führen. Beide Definitionen haben entscheidente Vor- aber auch Nachteile. Die Log-Returns werden aufgrund ihrer kumulierenten Summeneigenschaft sowie der damit engen Verbindung mit der Normalverteilung (zentraler Grenzwertsatz) häufig für zeitstetige Modelle oder Modelle mit Normalverteilungsannahmen verwendet. Insbesondere ist der Übergang von täglichen zu jährlichen Returns im Gegensatz zu den Renditen sehr einfach. Wir werden in Abschnitt Portfolio-Returns noch sehen, dass sich die Portfoliorendite von der Anfangsinvestition zum Zeitpunkt 0 bis nach \(t\) einfach als gewichtete Summe der Aktien-Renditen von \(0\) bis \(t\) berechnen lässt, was für Log-Returns nur näherungsweise gilt. Dennoch haben sich aber wegen der einfachereren Skalierbarkeit auch hier die Log-Returns gegenüber den Renditen durchgesetzt.
Werden allgemein die Aktienkurse zu den Zeitpunkten \(t_0, t_1, \ldots, t_n\) gemessen, so kann man die annualisierten Log-Returns (siehe Vorlesungsskript Portfoliooptimierung Prof. Pichler) definieren als
Dann muss allerdings auch der Gesamt-Return von \(t_0\) nach \(t_n\) annualisiert werden
Für äquividistante Tagesdaten wählt man \(t_i = \frac{i}{252}\), so dass \(t_{i}-t_{i-1} = \frac 1 {252}\), für Monatskurse \(\frac{i}{12}\) erhält man entsprechend \(t_i - t_{i-1} = \frac 1{12} \) und für Quartalsdaten \(t_i=\frac{i}{4}\) entsprechend \(t_i - t_{i-1} = \frac 1{4}\).
trading_days = 252
log_returns = np.log(renditen + 1)
mu = log_returns.mean()
mu_tilde = renditen.mean()
geo_mean_renditen = np.exp((np.log(renditen+1)).mean()) - 1
print("mean daily R %s: \n " % mu_tilde)
print("geometric mean daily R %s: \n" % geo_mean_renditen)
print("check: geo mean Rendite = exp(mean daily log)-1 = %s \n" % (np.exp(mu)-1) )
print("Daily log-returns %s: \n" % mu)
mean daily R Ticker
AAPL 0.000806
GOOGL 0.001141
MSFT 0.000879
TSLA 0.001609
dtype: float64:
geometric mean daily R Ticker
AAPL 0.000644
GOOGL 0.000950
MSFT 0.000744
TSLA 0.000870
dtype: float64:
check: geo mean Rendite = exp(mean daily log)-1 = Ticker
AAPL 0.000644
GOOGL 0.000950
MSFT 0.000744
TSLA 0.000870
dtype: float64
Daily log-returns Ticker
AAPL 0.000644
GOOGL 0.000950
MSFT 0.000744
TSLA 0.000870
dtype: float64:
#annualization: from daily means to yearly means
mu_year = mu * trading_days
mu_tilde_year = (mu_tilde+1) ** trading_days - 1
mu_tilde_year_approx1 = mu_tilde * trading_days
mu_tilde_year_approx2 = np.exp(mu_year) -1
#mu_tilde_year_approx3 = mu_year
print("mu year (Log-Returns r) %s: \n " % mu_year)
print("mu_tilde year (Renditen R) %s: \n " % mu_tilde_year)
print("Approximation1 : mu_tilde_year = mu_tilde * trading days %s\n " % (mu_tilde_year_approx1))
print("Approximation2: exp(mu_year) -1 %s: \n" % (mu_tilde_year_approx2))
mu year (Log-Returns r) Ticker
AAPL 0.162191
GOOGL 0.239331
MSFT 0.187527
TSLA 0.219225
dtype: float64:
mu_tilde year (Renditen R) Ticker
AAPL 0.225126
GOOGL 0.332960
MSFT 0.247735
TSLA 0.499430
dtype: float64:
Approximation1 : mu_tilde_year = mu_tilde * trading days Ticker
AAPL 0.203126
GOOGL 0.287566
MSFT 0.221427
TSLA 0.405410
dtype: float64
Approximation2: exp(mu_year) -1 Ticker
AAPL 0.176085
GOOGL 0.270399
MSFT 0.206263
TSLA 0.245112
dtype: float64:
from datetime import datetime
print(log_returns.head())
print(log_returns.tail())
annulalized_log_returns = log_returns.copy()
kalendertage = (annulalized_log_returns.index[-1] - annulalized_log_returns.index[0]).days
print("Tage insgesamt: %s, Jahre: %s" % (kalendertage, kalendertage / 365))
#annulalized_log_returns = annulalized_log_returns.reset_index()
for i in range(1,len(log_returns)):
annulalized_log_returns.iloc[i] = annulalized_log_returns.iloc[i]* 365 / (annulalized_log_returns.index[i] - annulalized_log_returns.index[i-1]).days
print(annulalized_log_returns.head() )
print(annulalized_log_returns.tail() )
mu_annulalized_log_returns = annulalized_log_returns.mean()
print(mu_annulalized_log_returns)
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 NaN NaN NaN NaN
2020-10-02 -0.032813 -0.021947 -0.029956 -0.076655
2020-10-05 0.030327 0.018534 0.020117 0.025193
2020-10-06 -0.029089 -0.021686 -0.021476 -0.027870
2020-10-07 0.016825 0.005580 0.018858 0.026977
Ticker AAPL GOOGL MSFT TSLA
Date
2025-09-24 -0.008367 -0.018124 0.001805 0.039008
2025-09-25 0.017912 -0.005477 -0.006135 -0.044802
2025-09-26 -0.005504 0.003047 0.008699 0.039390
2025-09-29 -0.004040 -0.010151 0.006120 0.006360
2025-09-30 0.000786 -0.003900 0.006489 0.003401
Tage insgesamt: 1825, Jahre: 5.0
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 NaN NaN NaN NaN
2020-10-02 -11.976715 -8.010804 -10.933771 -27.979088
2020-10-05 3.689752 2.254997 2.447606 3.065090
2020-10-06 -10.617315 -7.915267 -7.838799 -10.172617
2020-10-07 6.141062 2.036842 6.883342 9.846649
Ticker AAPL GOOGL MSFT TSLA
Date
2025-09-24 -3.054042 -6.615263 0.658820 14.238084
2025-09-25 6.537742 -1.999284 -2.239135 -16.352679
2025-09-26 -2.009045 1.112060 3.175206 14.377210
2025-09-29 -0.491551 -1.235049 0.744659 0.773835
2025-09-30 0.286821 -1.423583 2.368443 1.241436
Ticker
AAPL 0.218310
GOOGL 0.327925
MSFT 0.288035
TSLA 0.156346
dtype: float64
Sigma_day = log_returns.cov()
Sigma_year = Sigma_day * trading_days
Sigma_tilde_day = renditen.cov()
#Dummy zur simultanen Berechnung von (mu_tilde_i + 1)(mu_tilde_j + 1)
mu_tilde_dummy = np.outer(mu_tilde + 1, mu_tilde + 1)
#print(mu_tilde_dummy)
Sigma_tilde_year = (Sigma_tilde_day + mu_tilde_dummy)**trading_days - mu_tilde_dummy**trading_days
Sigma_tilde_year_approx1 = Sigma_year
Sigma_tilde_year_approx2 = Sigma_tilde_day * trading_days
print("Annualized covarince R (percentege changes)")
print(Sigma_tilde_year)
print("Approx1: Annualized covarince R (percentege changes): Sigma \approx Sigma_tilde")
print(Sigma_tilde_year_approx1)
print("Approx2: Annualized covarince R (percentege changes): yearly cov = daily cov * 252")
print(Sigma_tilde_year_approx2)
print("Annualized covariance r (log retunrs)")
print(Sigma_year)
Annualized covarince R (percentege changes)
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.128386 0.086467 0.075511 0.164824
GOOGL 0.086467 0.179404 0.092413 0.156668
MSFT 0.075511 0.092413 0.109105 0.128345
TSLA 0.164824 0.156668 0.128345 1.018243
Approx1: Annualized covarince R (percentege changes): Sigma pprox Sigma_tilde
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.081640 0.051456 0.048008 0.085705
GOOGL 0.051456 0.096250 0.054032 0.075453
MSFT 0.048008 0.054032 0.067629 0.066277
TSLA 0.085705 0.075453 0.066277 0.371477
Approx2: Annualized covarince R (percentege changes): yearly cov = daily cov * 252
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.082221 0.051700 0.048302 0.086148
GOOGL 0.051700 0.096431 0.054190 0.075684
MSFT 0.048302 0.054190 0.067863 0.066524
TSLA 0.086148 0.075684 0.066524 0.375040
Annualized covariance r (log retunrs)
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.081640 0.051456 0.048008 0.085705
GOOGL 0.051456 0.096250 0.054032 0.075453
MSFT 0.048008 0.054032 0.067629 0.066277
TSLA 0.085705 0.075453 0.066277 0.371477
log_returns_scaled = log_returns * trading_days
print(log_returns_scaled.mean())
print(mu_year)
print(log_returns_scaled.cov())
print(Sigma_year)
Ticker
AAPL 0.162191
GOOGL 0.239331
MSFT 0.187527
TSLA 0.219225
dtype: float64
Ticker
AAPL 0.162191
GOOGL 0.239331
MSFT 0.187527
TSLA 0.219225
dtype: float64
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 20.573373 12.967004 12.097986 21.597710
GOOGL 12.967004 24.255026 13.616010 19.014225
MSFT 12.097986 13.616010 17.042590 16.701709
TSLA 21.597710 19.014225 16.701709 93.612291
Ticker AAPL GOOGL MSFT TSLA
Ticker
AAPL 0.081640 0.051456 0.048008 0.085705
GOOGL 0.051456 0.096250 0.054032 0.075453
MSFT 0.048008 0.054032 0.067629 0.066277
TSLA 0.085705 0.075453 0.066277 0.371477
7.5. Portfolio-Returns#
Aus den obigen 4 Aktien stellen wir jetzt ein Portfolio zusammen und investieren 1000$ zu 10% in 1. Aktie (AAPL), 30% in 2. Aktie (MSFT), 40% in 3. Aktie (GOOGL) und die verbleibenden 20% in 4.Aktie (TSLA) und schichten das Portfolio im Folgenden nicht mehr um. Die Gewichtung ist hier übrigens absolut willkürlich gewählt und kann beliebig geändert werden. Uns interessiert die Wertentwicklung des Portfolios und daher betrachten wir die Portfolio-Returns bzw. Renditen.
Achtung wenn der Vektor mit den Aktientickern wie in diesem Beispiel nicht alphabetisch sortiert ist, kann es beim Zugriff auf die Kurs-Daten der Aktien und Returns Probleme mit der Reihenfolge geben, da die Download-Funktion von yfinance
diese automatisch sortiert und somit nicht mehr der ursprünglichen Reihenfolge entspricht. Beim Berechnen der Portfoliowerte und -Returns ist die Reihenfolge allerdings wesentlich. Greift man den DataFrame direkt auf dem Array der Ticker ab, erzwingt man die ursprüngliche Reihenfolge.
#vorgegebene Reihenfolge
print(tickers)
#automatische Sortierung
print(close_df.head())
#urspruengliche Reihenfolge
print(close_df[tickers].head())
calender_days = (close_df.index[-1] - close_df.index[0]).days
trading_days_per_year = 252
trading_days = len(close_df)
print("Kalendertage = %s, Handelstage = %s, Jahre (Handelstage) = %s, Jahre (Kalendertage) =%s"
% (calender_days, trading_days, trading_days / trading_days_per_year, calender_days / 365))
['AAPL', 'MSFT', 'GOOGL', 'TSLA']
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 113.603722 73.885399 203.712036 149.386673
2020-10-02 109.936546 72.281471 197.700226 138.363327
2020-10-05 113.321632 73.633644 201.717697 141.893326
2020-10-06 110.072754 72.054039 197.431763 137.993332
2020-10-07 111.940376 72.457253 201.190353 141.766663
Ticker AAPL MSFT GOOGL TSLA
Date
2020-10-01 113.603722 203.712036 73.885399 149.386673
2020-10-02 109.936546 197.700226 72.281471 138.363327
2020-10-05 113.321632 201.717697 73.633644 141.893326
2020-10-06 110.072754 197.431763 72.054039 137.993332
2020-10-07 111.940376 201.190353 72.457253 141.766663
Kalendertage = 1825, Handelstage = 1255, Jahre (Handelstage) = 4.98015873015873, Jahre (Kalendertage) =5.0
Wir nutzen dann später die Tages-Daten zur Schätzung der Kovarianz-Matrix und des Erwartungswertvektors der Returns, die wir zur Berechnung der erwarteten Portfolio-Returns sowie -Varianz benötigen.
Wird in \(t=0\) das Portfolio aus jeweils \(a_j\) Stocks zum Preis \(S_{j,0}\) zusammengesetzt (\(j=1, \ldots,J\)), gilt für den jeweiligen Portfolioanteil der \(j\)-ten Aktie
mit \(\sum\limits_{j=1}^J w_j = 1\), wobei \(P_0 = a_1 S_{1,0} + \ldots + a_J S_{J,0} = a^T S_0 = S_0^T a\) den investierte Gesamtbetrag beschreibt. Letztere Gleichung gilt für die Vektoren der Anzahl gekaufter Aktien \(a^T = (a_1, \ldots, a_J)\) und den Vektor der Startkurse \(S_0^T=(S_{1,0}, \ldots, S_{J,0})\) zur Investitionszeit in \(t=0\).
print(tickers)
weights = np.array([0.1, 0.3, 0.4, 0.2])
print("Summe der Portfoliogewichte = %f" % weights.sum())
#erzwingen urspruengliche Reihenfolge der Aktien im tickers vektor
portfolio = close_df[tickers].copy()
print(portfolio.head())
#vgl. sortiert
print(close_df.head())
#type pandas dataframe
print("Type: %s" % type(portfolio))
startkapital = 1000
S0 = portfolio.iloc[0] #1. Zeile entspricht Startwerten
stueckzahl = weights / S0 * startkapital
kaufpreis = sum(S0*stueckzahl)
print("kaufen in t=0 Stückzahl %s \n\n zum Preis %s \n\n für insgesamt %.1f Dollar" % (stueckzahl, S0, kaufpreis) )
#alternative Berechnung als Skalarprodukt:
S0.dot(stueckzahl)
['AAPL', 'MSFT', 'GOOGL', 'TSLA']
Summe der Portfoliogewichte = 1.000000
Ticker AAPL MSFT GOOGL TSLA
Date
2020-10-01 113.603722 203.712036 73.885399 149.386673
2020-10-02 109.936546 197.700226 72.281471 138.363327
2020-10-05 113.321632 201.717697 73.633644 141.893326
2020-10-06 110.072754 197.431763 72.054039 137.993332
2020-10-07 111.940376 201.190353 72.457253 141.766663
Ticker AAPL GOOGL MSFT TSLA
Date
2020-10-01 113.603722 73.885399 203.712036 149.386673
2020-10-02 109.936546 72.281471 197.700226 138.363327
2020-10-05 113.321632 73.633644 201.717697 141.893326
2020-10-06 110.072754 72.054039 197.431763 137.993332
2020-10-07 111.940376 72.457253 201.190353 141.766663
Type: <class 'pandas.core.frame.DataFrame'>
kaufen in t=0 Stückzahl Ticker
AAPL 0.880253
MSFT 1.472667
GOOGL 5.413790
TSLA 1.338808
Name: 2020-10-01 00:00:00, dtype: float64
zum Preis Ticker
AAPL 113.603722
MSFT 203.712036
GOOGL 73.885399
TSLA 149.386673
Name: 2020-10-01 00:00:00, dtype: float64
für insgesamt 1000.0 Dollar
1000.0
Damit können wir jetzt natürlich auch den Wert des Portfolios zu jedem späteren Zeitpunkt berechnen. Wird das Portfolio bis zum Zeitpunkt \(t>0\) nicht umgeschichtet, haben die Aktienkurse den Wert \(S_t^T = (S_{1,t}, \ldots, S_{J,t})\) und somit das Portfolio den Wert
Es bietet sich hierfür an die von Pandas bereitgestellten Matrix-Vektor-Operationen zu verwenden, denn die letzte Summe entspricht einfach dem Skalarprodukt zweier Vektoren[7] und die Berechnung der Spalte aller Portfoliowerte über die Zeit entspricht entsprechend einer Matrix-Vektor-Multiplikation, die in Pandas mittels der dot()
Funktion bereitgestellt wird (Doku Pandas). Wichtig ist allerdings, dass im DataFrame die Spalten in der gleichen Reihenfolge stehen, wie in den Vektoren - wir haben den DataFrame portfolio
extra auf die Ticker-Reihenfolge angepasst, alternativ kann auch immer automatsichen mittels portfolio[tickers]
explizit auf die Spalten der Aktien in der enstprechenden Reihenfolge zugegriffen werden.
#Umwandeln in Numpy-Array, um einfacher zu rechnen
print(stueckzahl)
stueckzahl_np = stueckzahl.to_numpy()
print(stueckzahl_np)
portfolio['Portfolio'] = portfolio.dot(stueckzahl_np)
#portfolio['Portfolio'] = portfolio[tickers].dot(stueckzahl_np)
print(portfolio.head())
Ticker
AAPL 0.880253
MSFT 1.472667
GOOGL 5.413790
TSLA 1.338808
Name: 2020-10-01 00:00:00, dtype: float64
[0.88025285 1.47266703 5.41378955 1.33880751]
Ticker AAPL MSFT GOOGL TSLA Portfolio
Date
2020-10-01 113.603722 203.712036 73.885399 149.386673 1000.000000
2020-10-02 109.936546 197.700226 72.281471 138.363327 964.477099
2020-10-05 113.321632 201.717697 73.633644 141.893326 985.419596
2020-10-06 110.072754 197.431763 72.054039 137.993332 962.475016
2020-10-07 111.940376 201.190353 72.457253 141.766663 976.888826
portfolio.plot()
<Axes: xlabel='Date'>

7.5.1. Berechnung der Portfolio-Rendite#
Für die Portfolio-Rendite, also den prozentualen Zuwachs bis zum Zeitpunkt \(t\) des Portfolios gilt wegen
d. h. die Portfolio-Rendite ergibt sich als die gewichtete Summe der Aktien-Renditen \(R_{j,t}=\frac{S_{j,t}}{S_{j,0}} - 1\) und kann bequem mittels Vektoren \(R_t = (R_{1,t}, \ldots, R_{J,t})^T\) und Gewichtsvektor \(w = (w_1, \ldots, w_J)^T\) auch wieder als Matrix-Vektor Produkt geschrieben werden. Für die Log-Returns gilt diese Beziehung zumindest näherungsweise, wie wir hier für unser Beispiel-Portfolio illustrieren.
#daily returns / taegliche Renditen von t-1, t
print(portfolio.head())
tickers1 = tickers.copy()
tickers1.append("Portfolio")
renditen = portfolio.pct_change()
renditen["Days_Diff"] = renditen.index.to_series().diff().dt.days
renditen[tickers1].plot()
print(renditen.head())
Ticker AAPL MSFT GOOGL TSLA Portfolio
Date
2020-10-01 113.603722 203.712036 73.885399 149.386673 1000.000000
2020-10-02 109.936546 197.700226 72.281471 138.363327 964.477099
2020-10-05 113.321632 201.717697 73.633644 141.893326 985.419596
2020-10-06 110.072754 197.431763 72.054039 137.993332 962.475016
2020-10-07 111.940376 201.190353 72.457253 141.766663 976.888826
Ticker AAPL MSFT GOOGL TSLA Portfolio Days_Diff
Date
2020-10-01 NaN NaN NaN NaN NaN NaN
2020-10-02 -0.032280 -0.029511 -0.021708 -0.073791 -0.035523 1.0
2020-10-05 0.030791 0.020321 0.018707 0.025513 0.021714 3.0
2020-10-06 -0.028670 -0.021247 -0.021452 -0.027485 -0.023284 1.0
2020-10-07 0.016967 0.019037 0.005596 0.027344 0.014976 1.0

Die Eigenschaft \(R_t^P = w^T R_t\) gilt allerdings nur für die Renditen von 0 bis t und nicht für die täglichen, wie die folgende Rechnung zeigt:
#Greifen explitzit nur die Spalten der Aktien-Ticker ab:
renditen["dot_version"] = renditen[tickers].dot(weights)
print(renditen.head())
#loeschen Spalte wieder (da falsch!)
renditen = renditen.drop(columns = ["dot_version"])
Ticker AAPL MSFT GOOGL TSLA Portfolio Days_Diff \
Date
2020-10-01 NaN NaN NaN NaN NaN NaN
2020-10-02 -0.032280 -0.029511 -0.021708 -0.073791 -0.035523 1.0
2020-10-05 0.030791 0.020321 0.018707 0.025513 0.021714 3.0
2020-10-06 -0.028670 -0.021247 -0.021452 -0.027485 -0.023284 1.0
2020-10-07 0.016967 0.019037 0.005596 0.027344 0.014976 1.0
Ticker dot_version
Date
2020-10-01 NaN
2020-10-02 -0.035523
2020-10-05 0.021761
2020-10-06 -0.023319
2020-10-07 0.015115
#kumulierte Renditen von 0..t
renditen_kum = renditen.copy()
renditen_kum[tickers1]=(1+renditen_kum[tickers1]).cumprod()-1
renditen_kum["Days"] = renditen_kum["Days_Diff"].cumsum()
renditen_kum["dot_version"] = renditen_kum[tickers].dot(weights)
renditen_kum["check"] = renditen_kum["Portfolio"] - renditen_kum["dot_version"]
renditen_kum["Portfolio-Log-Return"] = np.log(renditen_kum.Portfolio+1)
#print(renditen_kum.tail())
print(renditen_kum[["Portfolio","dot_version","check"]].head())
norm_check_renditen = np.linalg.norm(renditen_kum["check"].dropna(), ord=2) #euklid
##2-norm vs inf-norm
np.linalg.norm(renditen_kum["check"].dropna(), ord=2)
np.linalg.norm(renditen_kum["check"].dropna(), ord=np.inf)
ax = renditen_kum.plot(y=["Portfolio","dot_version","check","Portfolio-Log-Return"])
ax.lines[0].set_linestyle("--")
ax.lines[1].set_linestyle(":")
ax.lines[2].set_linestyle("-.")
ax.lines[3].set_linestyle("solid")
plt.show()
print("kumulierte Renditen: 2-Norm Fehler = %s (gerundet: %10f)" % (norm_check_renditen,np.round(norm_check_renditen, 10)))
Ticker Portfolio dot_version check
Date
2020-10-01 NaN NaN NaN
2020-10-02 -0.035523 -0.035523 1.318390e-16
2020-10-05 -0.014580 -0.014580 2.116363e-16
2020-10-06 -0.037525 -0.037525 2.220446e-16
2020-10-07 -0.023111 -0.023111 2.220446e-16

kumulierte Renditen: 2-Norm Fehler = 9.422882861920583e-14 (gerundet: 0.000000)
Wir sehen dass die beiden Spalten ‘Portfolio’, bei dem die Zuwachsraten von 0 bis t direkt aus den Portfoliowerten, und die Spalte ‘dot_version’, bei dem die kumulierten Portfolio-Renditen als gewichtete Summe wie in Formel eq-weigthed-ret
berechnet wurden bis auf minimale numerische Fehler (Spalte ‘check’ mit Norm nahe Null) tatsächlich übereinstimmen.
renditen_kum.plot(y=["check"])
<Axes: xlabel='Date'>

Ein Plot der kumulierten Returns aller einzelnen Aktien sowie des Portfolios zeigt die Diversifikationseffekte recht deutlich, denn die Volatilität der Tesla-Aktie, in die 20% investiert wurde, wird deutlich ausgeglättet, obwohl alle Renditen einen positiven Zusammenhang aufweisen.
renditen_kum[tickers1].plot()
<Axes: xlabel='Date'>

Die Berechnung der Log-Returns des Portfolios läuft analog (aus Bequemlichkeit rechnen wir hier einfach die Renditen in Log-Returns nach Formel () um, allerdings kann man wie oben gezeigt auch direkt die Log-Returns mittels diff()
Operator ausrechnen
log_returns = renditen.copy()
log_returns[tickers1] = np.log(renditen[tickers1] + 1)
log_returns[tickers1].plot()
plt.show()
print(log_returns.head())

Ticker AAPL MSFT GOOGL TSLA Portfolio Days_Diff
Date
2020-10-01 NaN NaN NaN NaN NaN NaN
2020-10-02 -0.032813 -0.029956 -0.021947 -0.076655 -0.036169 1.0
2020-10-05 0.030327 0.020117 0.018534 0.025193 0.021481 3.0
2020-10-06 -0.029089 -0.021476 -0.021686 -0.027870 -0.023559 1.0
2020-10-07 0.016825 0.018858 0.005580 0.026977 0.014865 1.0
log_returns2 = portfolio.copy()
log_returns2[tickers1] = np.log(log_returns2[tickers1]).diff()
print(log_returns2.head())
Ticker AAPL MSFT GOOGL TSLA Portfolio
Date
2020-10-01 NaN NaN NaN NaN NaN
2020-10-02 -0.032813 -0.029956 -0.021947 -0.076655 -0.036169
2020-10-05 0.030327 0.020117 0.018534 0.025193 0.021481
2020-10-06 -0.029089 -0.021476 -0.021686 -0.027870 -0.023559
2020-10-07 0.016825 0.018858 0.005580 0.026977 0.014865
Überganzg von täglichen Returns zu kumulierten Returns:
log_returns_kum = log_returns.copy()
log_returns_kum["Days"] = log_returns_kum["Days_Diff"].cumsum()
log_returns_kum[tickers1] = log_returns_kum[tickers1].cumsum()
log_returns_kum["dot_version"] = log_returns_kum[tickers].dot(weights)
log_returns_kum["check"] = log_returns_kum["Portfolio"] - log_returns_kum["dot_version"]
log_returns_kum["Portfolio-Rendite"] = np.exp(log_returns_kum.Portfolio) - 1
print(log_returns_kum[["Portfolio","dot_version","check"]].head())
norm_check_log_returns = np.linalg.norm(log_returns_kum["check"].dropna(), ord=2) #euklid
##2-norm vs inf-norm
np.linalg.norm(log_returns_kum["check"].dropna(), ord=2)
np.linalg.norm(log_returns_kum["check"].dropna(), ord=np.inf)
ax = log_returns_kum.plot(y=["Portfolio","dot_version","check","Portfolio-Rendite"])
ax.lines[0].set_linestyle("--")
ax.lines[1].set_linestyle(":")
ax.lines[2].set_linestyle("-.")
ax.lines[3].set_linestyle("solid")
plt.show()
print("kumulierte Log-Returns: 2-Norm Fehler = %s (gerundet: %10f)" % (norm_check_log_returns,np.round(norm_check_log_returns, 10)))
#log_returns_kum.head()
Ticker Portfolio dot_version check
Date
2020-10-01 NaN NaN NaN
2020-10-02 -0.036169 -0.036378 0.000209
2020-10-05 -0.014688 -0.014858 0.000170
2020-10-06 -0.038247 -0.038458 0.000211
2020-10-07 -0.023382 -0.023490 0.000108

kumulierte Log-Returns: 2-Norm Fehler = 0.39728058750717443 (gerundet: 0.397281)
log_returns_kum.plot(y=["check"])
<Axes: xlabel='Date'>

Wir sehen also, dass für die kumulierten Log-Returns des Portfolios zumindest näherungsweise \(r_t^P \approx w^T \cdot r_t\) gilt. Die Graphiken zeigen, dass der Fehler überschaubar ist.
7.5.2. Zufalls-Modell#
Wir haben im vorherigen AAbschnitt gesehen, dass für die Portfolio-Rendite \(R^P_t = w^t R_t\) gilt und für die Log-Returns diese Beziehung in sehr guter Näherung gilt. Aus diesem Grund wird der zufällige Portfolio-Return häufig mittels
unabhängig von der tatsächlich verwendeten Return-Definition angesetzt, wobei der Vektor der Aktien-Returns bis zum Zeitpunkt t als Zufallsvariable modelliert wird. Die Porfolio-Theorie beschäftigt sich mit einer geeigneten Wahl des Gewichtsvektors unter verschiedenen Risiko-Kriterien. Im klassichen Markowitz Modell geht man davon aus, dass die zufällige Rendite \(R_t^T = (R_{1,t},\ldots, R_{J,t}) \sim \mathcal{N}_J(\mu_t, \Sigma_t)\) aller Aktien einer multivariaten Normalverteilung genügt und daher durch die ersten beiden Momente \(\mu_t = \mathbb{E}R_t\) und \(\Sigma_t = \operatorname{cov}(R_t)\) vollständig das Zufallsverhalten charakterisieren. Für beliebig verteilte Zufallsvektoren reichen die ersten beiden Momente der Verteilung nicht. Wir haben oben gesehen, dass Log-Returns (zeitstetige Modelle) und die prozentualen Renditen (zeitdiskrete Modelle, arithmetische Returns) sich zwar leicht unterscheiden und aus mathematischer Sicht jeweils Vor- und Nachteile mitbringen. Während die Log-Returns aufgrund ihrer Additivität eher mit der Normalverteilungsannahme[8] einhergehen, folgen die Renditen dann wegen Formel () einer Log-Normalverteilung.
Ein weiterer Vorteil der Log-Returns gegenüber den Renditen ist die einfachere Skalierbarkeit der ersten beiden Momente \(\Sigma\) und \(\mu\), so dass diese aus den vorhandenen Tages-Returns geschätzt werden können und dann wie in Abschnitt Annualisierte Returns beschrieben, einfach auf den Anlagezeitraum von 0 bis t skaliert werden können. Daher werden häufig die Log-Returns in der Finanzmathematik und insbesondere in der Portfolio-Theorie verwendet. Häufig unterstellt man für effiziente Finanzmärkte die Unkorreliertheit der Tages-Returns zwischen verschiedenen Handelstagen, wie wir das auch für die Skalierung der annualizierten Momente ausgenutzt haben, so dass alle relevanten und verfügbaren Informationen über die Aktiengesellschaften tatsächlich schon in den aktuellen Kursen eingepreist sind.
7.5.3. Portfolioanalyse: erwarteter Portfolio-Return und Portfolio-Varianz#
Wegen Formel (7.14) können wir die erwartete Rendite \(\mathbb{E} R^P_t = \mu_t ^T w\) sowie die Portfolio-Varianz einfach mittels der ersten beiden Momente des Zufalls-Vektors \(R_t^T = (R_{1,t}, \ldots, R_{J,t})\) der Aktien-Returns \(\mu_t = \mathbb{E} R_t\) sowie \(\Sigma_t = \operatorname(R_t) = \mathbb{E} \left( R_t - \mathbb{E} R_t \right) \left(R_t - \mathbb{E} R_t \right)^T\) ableiten
Üblicherweise werden wie in Annualisierte Returns beschrieben aus den Tages-Log-Returns mit Hilfe des arithmetsichen Mittels die erwarteten Tages-Returns bzw. Renditen geschätzt sowie deren Kovarianzmatrix berechnet und anschließend durch Skalierung annualisiert. Verwendet man die Renditen nutzt den Zusammenhang ziwschen arithmetischen und geometrischen Mittel zur Annualisierung. Da wir streng genommen diese Annualisierung nur für die Log-Returns einfach berechnen können, kann man aber danach wieder zu den Renditen mittels Formel () übergehen. In der Praxis wird das nicht immer so konsequent unterschieden, aber wir haben ja gesehen, dass die Unterschiede zwischen Log-Returns und Renditen für kleinere Zeiträume nicht so deutlich sind, die folgenden Rechnungen zeigen dennoch die Probleme bei der Approximation auf. Aus diesem Grund wird tatsächlich bevorzugt mit den Log-Returns in der Portfolio-Theorie gearbeitet.
mu_day = log_returns[tickers].mean()
mu_year = mu_day * trading_days_per_year
print(mu_year)
Ticker
AAPL 0.162191
MSFT 0.187527
GOOGL 0.239331
TSLA 0.219225
dtype: float64
mu_tilde_day = renditen[tickers].mean()
geo_mean_renditen = np.exp((np.log(renditen[tickers]+1)).mean()) - 1
print("mean daily R %s: \n " % mu_tilde_day)
print("geometric mean daily R %s: \n" % geo_mean_renditen)
mean daily R Ticker
AAPL 0.000806
MSFT 0.000879
GOOGL 0.001141
TSLA 0.001609
dtype: float64:
geometric mean daily R Ticker
AAPL 0.000644
MSFT 0.000744
GOOGL 0.000950
TSLA 0.000870
dtype: float64:
print("check: geo mean Rendite = exp(mean daily log)-1 = %s \n" % (np.exp(mu_day)-1) )
print("Daily log-returns %s: \n" % mu_day)
check: geo mean Rendite = exp(mean daily log)-1 = Ticker
AAPL 0.000644
MSFT 0.000744
GOOGL 0.000950
TSLA 0.000870
dtype: float64
Daily log-returns Ticker
AAPL 0.000644
MSFT 0.000744
GOOGL 0.000950
TSLA 0.000870
dtype: float64:
Wir sehen Abweichungen zwischen den geometrischen und arithmetischen Mittelwerten der täglichen Renditen bzw. Log-Returns. Außerdem sehen wir den den nichtlineare Zusammenhang zwischen mittleren Log-Returns und geometrischen Mittel der Renditen. Nun werden diese Mittel annualisiert.
mu_tilde_year = (mu_tilde_day[tickers]+1) ** trading_days_per_year - 1
mu_tilde_year_approx1 = mu_tilde_day * trading_days_per_year
mu_tilde_year_approx2 = np.exp(mu_year) -1
print("mu year (Log-Returns r) %s: \n " % mu_year)
print("mu_tilde year (Renditen R) %s: \n " % mu_tilde_year)
print("Approximation1 für Renditen: mu_tilde_year = mu_tilde * trading days_per_year %s\n " % (mu_tilde_year_approx1))
print("Approximation2 für Renditen: exp(mu_year) -1 %s: \n" % (mu_tilde_year_approx2))
mu year (Log-Returns r) Ticker
AAPL 0.162191
MSFT 0.187527
GOOGL 0.239331
TSLA 0.219225
dtype: float64:
mu_tilde year (Renditen R) Ticker
AAPL 0.225126
MSFT 0.247735
GOOGL 0.332960
TSLA 0.499430
dtype: float64:
Approximation1 für Renditen: mu_tilde_year = mu_tilde * trading days_per_year Ticker
AAPL 0.203126
MSFT 0.221427
GOOGL 0.287566
TSLA 0.405410
dtype: float64
Approximation2 für Renditen: exp(mu_year) -1 Ticker
AAPL 0.176085
MSFT 0.206263
GOOGL 0.270399
TSLA 0.245112
dtype: float64:
Die Auswirkungen auf die erwartete jährliche Portfoliorendite je nach Berechnungsart:
print("erwartete annualisierte Portfoliorendite: \n"
" %f (mu_tilde_year -- Rendite correct) \n"
" %f (mu_year -- Log-Returns) \n"
" %f (Approx1 Rendite) \n"
" %f (Approx2 Rendite) \n" %
(mu_tilde_year.dot(weights),mu_year.dot(weights), mu_tilde_year_approx1.dot(weights), mu_tilde_year_approx2.dot(weights)))
erwartete annualisierte Portfoliorendite:
0.329903 (mu_tilde_year -- Rendite correct)
0.212055 (mu_year -- Log-Returns)
0.282849 (Approx1 Rendite)
0.236670 (Approx2 Rendite)
Die 1. Approximation liefert bessere Ergebnisse für die Renditen, allerdings ist dieser Ansatz bei der Approximation der Kovarianz zu grob.
Kovarianz- und Korrelationsmatzrix für Log-Returns
Sigma_day = log_returns[tickers].cov()
Sigma_year = Sigma_day * trading_days_per_year
print("Tages vs. Jahres-Kovarianz für Log-Returns")
print(Sigma_day)
print(Sigma_year)
Tages vs. Jahres-Kovarianz für Log-Returns
Ticker AAPL MSFT GOOGL TSLA
Ticker
AAPL 0.000324 0.000191 0.000204 0.000340
MSFT 0.000191 0.000268 0.000214 0.000263
GOOGL 0.000204 0.000214 0.000382 0.000299
TSLA 0.000340 0.000263 0.000299 0.001474
Ticker AAPL MSFT GOOGL TSLA
Ticker
AAPL 0.081640 0.048008 0.051456 0.085705
MSFT 0.048008 0.067629 0.054032 0.066277
GOOGL 0.051456 0.054032 0.096250 0.075453
TSLA 0.085705 0.066277 0.075453 0.371477
Kovarianz- und Korrelationsmatzrix für Renditen
Sigma_tilde_day = renditen[tickers].cov()
#Dummy zur simultanen Berechnung von (mu_tilde_i + 1)(mu_tilde_j + 1)
mu_tilde_dummy = np.outer(mu_tilde + 1, mu_tilde + 1)
#print(mu_tilde_dummy)
Sigma_tilde_year = (Sigma_tilde_day + mu_tilde_dummy)**trading_days_per_year - mu_tilde_dummy**trading_days_per_year
Sigma_tilde_year_approx1 = Sigma_tilde_day * trading_days_per_year
#Delta Method:
exp_mu_year = np.exp(mu_year)
Sigma_tilde_year_approx2 = (Sigma_year.dot(np.diag(exp_mu_year))).dot(np.diag(exp_mu_year))
#(Sigma_year.dot(weights)).dot(weights)
print("Tages vs. Jahres-Kovarianz für R")
print(Sigma_tilde_day)
print(Sigma_tilde_year)
print("Approx1 für R: Sigma \approx Sigma_tilde")
print(Sigma_tilde_year_approx1)
print("Approx2 für R: cov(R) = cov(e^r - 1) = cov(e^r) approx e^r cov(r) (e^r)^T (Delta-Methode für R_t = exp(r_t) - 1)")
print(Sigma_tilde_year_approx2)
Tages vs. Jahres-Kovarianz für R
Ticker AAPL MSFT GOOGL TSLA
Ticker
AAPL 0.000326 0.000192 0.000205 0.000342
MSFT 0.000192 0.000269 0.000215 0.000264
GOOGL 0.000205 0.000215 0.000383 0.000300
TSLA 0.000342 0.000264 0.000300 0.001488
Ticker AAPL MSFT GOOGL TSLA
Ticker
AAPL 0.128386 0.080647 0.080960 0.164824
MSFT 0.080647 0.124452 0.092413 0.137074
GOOGL 0.080960 0.092413 0.157283 0.146691
TSLA 0.164824 0.137074 0.146691 1.018243
Approx1 für R: Sigma pprox Sigma_tilde
Ticker AAPL MSFT GOOGL TSLA
Ticker
AAPL 0.082221 0.048302 0.051700 0.086148
MSFT 0.048302 0.067863 0.054190 0.066524
GOOGL 0.051700 0.054190 0.096431 0.075684
TSLA 0.086148 0.066524 0.075684 0.375040
Approx2 für R: cov(R) = cov(e^r - 1) = cov(e^r) approx e^r cov(r) (e^r)^T (Delta-Methode für R_t = exp(r_t) - 1)
0 1 2 3
Ticker
AAPL 0.112923 0.069855 0.083046 0.132869
MSFT 0.066403 0.098405 0.087203 0.102749
GOOGL 0.071173 0.078620 0.155339 0.116975
TSLA 0.118545 0.096437 0.121775 0.575903
Die deutlich bessere 2. Approximation basiert auf der Delta-Methode .
Portvoliovarianz berechnet:
Für die Log-Returns
P_log_var_year = (Sigma_year.dot(weights)).dot(weights)
print(P_log_var_year)
#alternative Berechnung mittels matmult
print(np.matmul(weights.transpose(), np.matmul(Sigma_year, weights)))
0.08058069019243778
0.08058069019243778
Für die Renditen
P_var_year = (Sigma_tilde_year.dot(weights)).dot(weights)
print(P_var_year)
0.15838652599719283
P_var_year_approx1 = (Sigma_tilde_year_approx1.dot(weights)).dot(weights)
P_var_year_approx2 = (Sigma_tilde_year_approx2.dot(weights)).dot(weights)
print(P_var_year_approx1)
print(P_var_year_approx2)
0.0809384383256009
0.12411087763881887
print("annualisierte Varianz der Portfolio-Rendite: \n"
" %f (korrekt) \n"
" %f (Approx1) \n"
" %f (Approx2) \n" %
(P_var_year, P_var_year_approx1, P_var_year_approx2))
annualisierte Varianz der Portfolio-Rendite:
0.158387 (korrekt)
0.080938 (Approx1)
0.124111 (Approx2)