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\).

Rendite vs. Log-Returns#

👉 Renditen beschreiben den prozentualer Wertzuwachs von \(a\) nach \(t\):

\[ R_{a,t} = \frac{S_{t} - S_{a}}{S_a}= \frac{S_t}{S_a}-1 \]

und werden auch als diskrete oder arithmetische Returns bezeichnet. In der Finanzmathematik arbeitet man hingegen häufig mit den Log-Returns:

\[ r_{a,t} = \log{\left( \frac{S_t}{S_a} \right)} = \log {\left(S_t \right)} - \log {\left(S_a \right)}, \]

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

()#\[ R_{a,t}=e^{r_{a,t}}-1 \Leftrightarrow r_{a,t} = \log{\left( R_{a,t}+1\right)} \]

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_3383200/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   50823

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()
#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'>
_images/508f4d377b8b6c16b7e22eca45781977579b1413eacc618998591e1974c5cc58.png

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()
_images/8860d61176e0ae1eaf5fa10ec3b81283c2ffa13bb554ab236ec426469b7292ef.png

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\)

\[ r_{t-1,t} = \log{\left( \frac{S_t}{S_{t-1}} \right)} \approx \frac{S_t}{S_{t-1}} - 1 = R_{t-1,t} \]

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.

Wertzuwachs über die Zeit: kumulierte Returns vs. Renditen#

Die Log-Returns des Zuwachses von der Anfangsinvestition bis zum Zeitpunkt \(t\) ist wegen

()#\[\begin{split}\begin{aligned} r_{0,t} &= \log(S_t) - \log(S_0) = \log(S_t) - \log(S_{t-1}) + \log(S_{t-1}) \pm \ldots + \log(S_1)-\log(S_0)\\ &=r_{t-1,t} + \ldots + r_{0,1} \end{aligned}\end{split}\]

gerade die Summe der täglichen Log-Returns. Somit folgt dann für die Renditen eine Produkt-Struktur

()#\[ R_{0,t}+1 = \frac{S_t}{S_0}=e^{r_0,t}=e^{\sum\limits_{k=1}^t r_{k-1,k}} = \prod\limits_{k=1}^t e^{r_{k-1,k}} = \prod\limits_{k=1}^t \left(R_{k-1,k} +1 \right) \]

also ähnlich wie beim (geometrischen) Verzinsen mit Zinseszinseffekten. 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 0x7fb1612b71d0>
_images/59726be4cf16cdcef412b21a6bbeea4bbc56686cea1124c2b9f3fe364fe0d712.png

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

Annualisierte Returns#

Aus Gleichung () folgt

()#\[ R_{0,t} = \prod\limits_{k=1}^t \left(R_{k-1,k} +1 \right) - 1 = y_\text{geo}^t - 1 \]

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\)

\[ y_\text{geo} = \sqrt[n]{\prod\limits_{k=1}^n y_k} = \left(\prod\limits_{k=1}^n y_k \right)^\frac 1n = \exp{\left( \frac 1n \sum\limits_{k=1}^n \log{y_k} \right)} = \exp{ \left(\overline{\log{y}}\right)} \]

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 () schreiben als

\[ r_{0,t} = \sum\limits_{k=1}^t r_{k-1,k} = t \bar{x} \]

Außerdem gilt folgender Zusammenhang zwischen geometrischen Mittel der Renditen und arithmetischen Mittel der Log-Returns für beliebige Stichprobengröße \(n\)

\[\begin{align*} (R+1)_\text{geo} &= y_\text{geo} = \left(\prod\limits_{k=1}^n y_k \right)^\frac 1n = \exp{\left( \frac 1n \sum\limits_{k=1}^n \log{y_k} \right)} = \exp{\left( \frac 1n \sum\limits_{k=1}^n \log{ \left( R_{k-1,k} + 1\right)} \right)} \\ &= \exp{\left( \frac 1n \sum\limits_{k=1}^n r_{k-1,k} \right)} = \exp{\left( \frac 1n \sum\limits_{k=1}^n x_k \right)} = \exp{\left( \overline x \right)} = \exp{\left( \overline r \right)} \end{align*}\]

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

()#\[ r_\text{annualized} = \bar{r}_\text{daily} \cdot 252 \]

sowie

()#\[\begin{align}\label{eq:rendite-annualized} R_\text{annualized} &= (1 + R)_\text{geo,daily}^{252} - 1 = \exp{(r_\text{annualized})} - 1\\ & = \exp{\left( \bar{r}_\text{daily} \cdot 252 \right)} - 1 % \approx \bar{r}_\text{daily} \cdot 252 \approx \bar{R}_\text{daily} \cdot 252 \end{align}\]

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 \(i\)-ten Aktie \(r_{i,(k-1,k)}\) unabhängig und identisch verteilte Zufallsvariablen (iid) mit \(\mathbb{E} r_{i,(k-1,k)} = \mu_i\) und \(\operatorname{Var} r_{i,(k-1,k)}=\sigma^2_i\) sind, folgt für die erwarteten Jahres-Log-Returns sowie deren Varianzen bzw. Volatilitäten / Standardabweichungen

\[\begin{align*} %\label{eq:expectation-year-log-return} \mathbb{E} r_{i, \text {annualized}} &= 252 \mu_i = 252 \cdot \mathbb{E} r_{i,\text{daily}}\\ \operatorname{Var}(r_{i,\text {annualized}}) &= 252 \sigma^2_i = 252 \cdot \operatorname{Var}(r_{i,\text {daily}})\\ \operatorname{SD}(r_\text {annualized}) &= \sqrt{252} \sigma \end{align*}\]

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.

\[\begin{align*} X_k &:= r_{i,(k-1,k)} := \log {\left( \frac{S_{i,k}}{S_{i,k-1}} \right)} = \log S_{i,k} - \log S_{i,k-1}\\ Y_l &:= r_{j,(l-1,l)} := \log {\left( \frac{S_{j,l}}{S_{j,l-1}} \right)} = \log S_{j,l} - \log S_{j,l-1}\\ \end{align*}\]

Mit () folgt daher für die Jahres-Returns (für \(t=252\))

\[\begin{align*} r_{i,(0,t)} &= \log {\left( \frac{S_{i,t}}{S_{i,0}} \right)} = \sum\limits_{k=1}^t X_k\\ r_{j,(0,t)} &= \log {\left( \frac{S_{j,t}}{S_{j,0}} \right)} = \sum\limits_{l=1}^t Y_l\\ \end{align*}\]

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

\[\begin{split} \operatorname{Cov}(X_k,Y_l) = \operatorname{Cov}(r_{i,(k-1,k)},r_{j,(l-1,l)}) = \begin{cases} 0, & \text{ falls } k \neq l\\ \sigma_{ij}, & \text{ falls } k = l \end{cases} \end{split}\]

erhalten wir für die Kovarianz der Jahres-Returns zwischen der \(i\)-ten und \(j\)-ten Aktie

()#\[\begin{align}\label{eq:Kov-Jahres-Returns} \operatorname{Cov}(r_{i,(0,t)} ,r_{j,(0,t)} ) &= \operatorname{Cov} \left(\sum\limits_{k=1}^t X_k, \sum\limits_{l=1}^t Y_l\right) = \sum\limits_{k=1}^t \sum\limits_{l=1}^t \operatorname{Cov}(X_k,Y_l)\\ &= \sum\limits_{k=1}^t \operatorname{Cov}(r_{i,(k-1,k)},r_{j,(k-1,k)}) = t \sigma_{ij}. \end{align}\]

Folglich erhalten wir für die annualisierte Kovarianzmatrik der Log-Returngs

\[ \Sigma_\text {annualized} = 252 \cdot \Sigma_\text{daily}, \]

wobei \(\Sigma_\text{daily}\) die Kovarianzmatrix der Tages-Returns bezeichnet.

Die Annualisierung für die prozentualen Zuwächse ist wegen der Nichtlinearität in () komplizierter. Analog zu den Log-Returns führen wir für die zufälligen Renditen folgende Bezeichnungen ein

\[\begin{align*} \tilde{X}_k &:= R_{i,(k-1,k)} + 1 = \frac{S_{i,k}}{S_{i,k-1}} \\ \tilde{Y}_l &:= R_{j,(l-1,l)} +1 = \frac{S_{j,l}}{S_{j,l-1}}, \end{align*}\]

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

\[\begin{split} \operatorname{Cov}(\tilde{X}_k,\tilde{Y}_l) = \operatorname{Cov}(R_{i,(k-1,k)}+1,R_{j,(l-1,l)}+1) = \operatorname{Cov}(R_{i,(k-1,k)},R_{j,(l-1,l)}) = \begin{cases} 0, & \text{ falls } k \neq l\\ \tilde{\sigma}_{ij}, & \text{ falls } k = l \end{cases} \end{split}\]

Aus Formel () folgt dann für die erwartete Jahres-Rendite und Varianz

\[\begin{align*} \mathbb{E} R_{i,(0,t)} &= \mathbb{E} \left( \prod\limits_{k=1}^t \left(R_{i,(k-1,k)} +1 \right) - 1 \right)\\ &= \prod\limits_{k=1}^t \left( \mathbb{E}R_{i,(k-1,k)} +1 \right) -1 \\ &= \left( \prod\limits_{k=1}^t \left(\mathbb{E} \tilde{X}_k \right) - 1 \right) = \prod\limits_{k=1}^t \left( \tilde{\mu}_i + 1 \right) -1 = (\tilde{\mu}_i+1)^t - 1\\ \operatorname{Var}(R_{i,(0,t)}) &= \operatorname{Var}\left( \prod\limits_{k=1}^t \tilde{X}_k -1 \right) = \operatorname{Var}\left( \prod\limits_{k=1}^t \tilde{X}_k \right)\\ &= \mathbb{E}\left( \prod\limits_{k=1}^t \tilde{X}_k \right)^2 - \left(\mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X_k} \right) \right)^2 \\ &=\mathbb{E}\left( \prod\limits_{k=1}^t \tilde{X}_k^2 \right) - \prod\limits_{k=1}^t \left( \mathbb{E} \tilde{X}_k \right)^2\\ &= \left( \tilde{\mu}_i^2 + \tilde{\sigma}^2_i + 2 \tilde{\mu}_i + 1 \right)^t - (\tilde{\mu}_i+1)^{2t} \end{align*}\]

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 entsprechend

\[\begin{align*} \operatorname{Cov}(R_{i,(0,t)} ,R_{j,(0,t)} ) &= \operatorname{Cov}(R_{i,(0,t)} +1,R_{j,(0,t)} +1 )\\ &= \operatorname{Cov} \left(\prod\limits_{k=1}^t \tilde{X}_k, \prod\limits_{l=1}^t \tilde{Y}_l\right)\\ &= \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k - \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k \right)\right) \left( \prod\limits_{l=1}^t \tilde{Y}_l - \mathbb{E} \left( \prod\limits_{l=1}^t \tilde{Y}_l \right)\right)\\ &= \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k - \prod\limits_{k=1}^t \mathbb{E}\tilde{X}_k \right) \left( \prod\limits_{l=1}^t \tilde{Y}_l - \prod\limits_{l=1}^t \mathbb{E}\tilde{Y}_l \right)\\ &= \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k - (\tilde{\mu_i}+1)^t \right) \left( \prod\limits_{l=1}^t \tilde{Y}_l - (\tilde{\mu}_j+1)^t\right)\\ &= \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k \prod\limits_{l=1}^t \tilde{Y}_l \right) - (\tilde{\mu}_j+1)^t \mathbb{E} \left( \prod\limits_{k=1}^t \tilde {X}_k \right)\\ & \quad - (\tilde{\mu}_i+1)^t \mathbb{E} \left( \prod\limits_{l=1}^t \tilde{Y}_l \right) + (\tilde{\mu}_i+1)^t (\tilde{\mu}_j+1)^t\\ &= \mathbb{E} \left( \prod\limits_{k=1}^t \tilde{X}_k \tilde{Y}_k \right) - (\tilde{\mu}_i+1)^t (\tilde{\mu}_j+1)^t \\ &= \prod\limits_{k=1}^t \mathbb{E} \left( \tilde{X}_k \tilde{Y}_k \right) - (\tilde{\mu}_i+1)^t (\tilde{\mu}_j+1)^t \\ &= \left( \tilde{\sigma}_{ij} + \tilde{\mu_i}\tilde{\mu_j} + \tilde{\mu}_i + \tilde{\mu_j} + 1\right)^t - (\tilde{\mu}_i+1)^t (\tilde{\mu}_j+1)^t \end{align*}\]

wobei die letzte Gleichung aus

\[\begin{align*} \tilde{\sigma}_{ij} &= \operatorname{Cov}(R_{i,(k-1,k)}, R_{j,(k-1,k)}) = \operatorname{Cov}(R_{i,(k-1,k)}+1, R_{j,(k-1,k)}+1)\\ &=\operatorname{Cov}(\tilde{X}, \tilde{Y})= \mathbb{E} \left(\tilde X \tilde Y \right) - \mathbb{E} \tilde{X} \cdot \mathbb{E} \tilde{Y} = \mathbb{E} \left(\tilde X \tilde Y \right) - (\tilde{\mu}_i +1)(\tilde{\mu}_j +1) \end{align*}\]

folgt.

Prinzipiell können wir also mit diesen Formeln von dem täglichen Erwartungswertvektor bzw. Kovarianzmatrix der Renditen übergehen zu den annualisierten Werten, allerdings nicht so einfach wie für die Log-Returns.

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 0x7fb160a3ba10>
_images/cd3b2327171fab4828a8f92997149641b88f425e9c79a9dac160ace087661dec.png
#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 0x7fb159f19e90>
_images/128c993e3529a566e049924c6469f2203583b2473a6cf7d1a0f66f9a47c83879.png

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 filer() 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_3383200/1356242698.py:1: FutureWarning: The provided callable <function mean at 0x7fb1a011c4a0> 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_3383200/1356242698.py:1: FutureWarning: The provided callable <function std at 0x7fb1a011c5e0> 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

höhere Momente: Schiefe und Kurtosis#

Allgemein kann die Verteilung einer Stichprobe mittels der empirischen zentralen bzw. allgemeinen Momente

\[ \tilde{M}_k = \frac 1n \sum\limits_{i=1}^n \left( x_i - \bar{x} \right)^k \qquad \text{ bzw. } \qquad M_k = \sum\limits_{i=1}^n x_i^k \]

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

\[ \tilde{M}_2 = \tilde{s}^2 = \frac 1n \sum\limits_{i=1}^n (x_i - \overline x)^2 = \frac 1n \sum\limits_{i=1}^n x_i^2 - \overline{x}^2 = M_2 - M_1^2 \]

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

\[ \tilde{M}_2(z) = \frac 1n \sum\limits_{i=1}^n (z_i - \overline{z})^2 = \frac 1n \sum\limits_{i=1}^n z_i^2 = \frac 1n \sum\limits_{i=1}^n \left( \frac{x_i - \overline{x} }{\tilde s} \right) ^2 = \frac{\frac 1n \sum\limits_{i=1}^n \left( x_i - \overline x \right)^2}{\tilde s^2} = \frac{\tilde{M}_2(x)}{\tilde{M}_2(x)}= 1 \]
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

\[ g = \frac{\tilde{M}_3(x)}{\tilde{M}_2(x)^\frac 32} = \frac{\frac{1}{n}\sum\limits_{i=1}^n \left( x_i - \bar{x} \right)^3}{\sqrt{ \left( \frac{1}{n}\sum\limits_{i=1}^n \left( x_i - \bar{x} \right)^2 \right)^3}} = \frac{ \left(\frac 1n \sum\limits_{i=1}^n (x_i - \bar{x}) \right)^3}{\tilde s^3} = \frac 1n \sum\limits_{i=1}^n \left( \frac{x_i - \overline x}{\tilde s} \right)^3 = \tilde{M}_3(z). \]

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

\[ \gamma = \frac{\tilde{M}_4(x)}{\tilde{M}_2^2(x)}-3 = \frac{\frac{1}{n}\sum\limits_{i=1}^n \left( x_i - \bar{x} \right)^4}{\left( \frac{1}{n}\sum\limits_{i=1}^n \left( x_i - \bar{x} \right)^2 \right)^2} - 3 = \frac 1n \sum\limits_{i=1}^n \left( \frac{x_i - \bar{x}}{\tilde s} \right)^4 -3 = \tilde{M}_4(z) -3 \]

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.

Test auf Normalverteilung#

to do

  • graphische Analyse mit Histogramm und qq-plots

  • Shapiro Wilk, KS-Test, Liliefors Korrektur, usw.

  • scipy: normaltest

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…

Abhängigkeiten zwischen Aktien-Returns: Kovarianz und Korrelation#

Bevor wir Portfolio-Returns untersuchen, analysieren wir noch die Abhängigkeiten zwischen den einzelnen Aktien-Renditen.

tickers = ["AAPL", "MSFT", "GOOGL", "TSLA"]

# Kurse herunterladen (letztes Jahr)
portfolio_data = yf.download(tickers, period="2y")  
#print(data.head())
close_df = portfolio_data["Close"].copy()

# Index (Datum) zurück als Spalte holen
#close_df = close_df.set_index('Date')

print(close_df.head())
close_df.plot()
/tmp/ipykernel_3383200/1712810207.py:4: FutureWarning: YF.download() has changed argument auto_adjust default to True
  portfolio_data = yf.download(tickers, period="2y")
[                       0%                       ]
[**********************50%                       ]  2 of 4 completed
[**********************75%***********            ]  3 of 4 completed
[*********************100%***********************]  4 of 4 completed
Ticker            AAPL       GOOGL        MSFT        TSLA
Date                                                      
2023-10-02  172.064651  133.250961  317.022461  251.600006
2023-10-03  170.727737  131.522858  308.737366  246.529999
2023-10-04  171.975510  134.313644  314.224701  261.160004
2023-10-05  173.213409  134.144775  314.618683  260.049988
2023-10-06  175.768387  136.637604  322.401428  260.529999

<Axes: xlabel='Date'>
_images/2331ad84947e2053f1970899ab45c1e117cba09a4401b90111e1f04b53ed9c50.png

Wir interessieren uns für den Zusammenhang der Renditen dieser Aktien

renditen = close_df.pct_change()

print(renditen.head())
renditen.plot(alpha = 0.7)
Ticker          AAPL     GOOGL      MSFT      TSLA
Date                                              
2023-10-02       NaN       NaN       NaN       NaN
2023-10-03 -0.007770 -0.012969 -0.026134 -0.020151
2023-10-04  0.007309  0.021219  0.017773  0.059344
2023-10-05  0.007198 -0.001257  0.001254 -0.004250
2023-10-06  0.014750  0.018583  0.024737  0.001846
<Axes: xlabel='Date'>
_images/34071c1c990c7dab9df537f8bce92ae5ce06abafbc99e62821886f30d4b3c026.png

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()
_images/d4a28f4e5bfb2af2940ee541e6dd6a32715f5d29111a0e4ceafdfd95de7a22dd.png
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)
_images/725fdc24516754ba19fa4a17bbbe7b4acade1d7baa97de8ca879fe640296ccfe.png

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'>
_images/5a7e50d5e969c98738b9eea8da4aaa65effada2ade13f1cad1f4f76285bd7f87.png
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 0x7fb13ed53010>
_images/4959c4cddead6cb6885464748bdddea990027db01584905d857359eca12a6b1c.png

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

\[ s_{xy} = \frac1{n-1} \sum\limits_{i=1}^n (x_i - \overline x)(y_i - \overline y) = \frac 1{n-1} \left(\sum\limits_{i=1}^n x_i y_i - n \overline x \overline y \right) \]

berechnet. Die Korrelation entsteht durch Normierung und ist einfacher zu interpretieren

\[\begin{align*} r_{xy} &= \frac{s_{xy}}{s_x \dot s_y} = \frac{\sum\limits_{i=1}^n (x_i - \overline x)(y_i - \overline y)}{\sqrt{\sum\limits_{i=1}^n (x_i - \overline x)^2 \cdot \sum\limits_{i=1}^n (y_i - \overline y)^2}}\\ &= \frac{\sum\limits_{i=1}^n x_i y_i - n \overline x \overline y}{\sqrt{\left(\sum\limits_{i=1}^n x_i^2 - n \overline x^2\right) \left(\sum\limits_{i=1}^n y_i^2 - n \overline y^2 \right)}} \in [-1,1] \end{align*}\]

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.

kovarianz = renditen.cov()
print(kovarianz)
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.000309  0.000151  0.000125  0.000314
GOOGL   0.000151  0.000360  0.000136  0.000291
MSFT    0.000125  0.000136  0.000198  0.000212
TSLA    0.000314  0.000291  0.000212  0.001593
korrelation = renditen.corr()
print(korrelation)
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    1.000000  0.451297  0.504945  0.448455
GOOGL   0.451297  1.000000  0.510227  0.383755
MSFT    0.504945  0.510227  1.000000  0.376302
TSLA    0.448455  0.383755  0.376302  1.000000

Zum Abschluss zeichnen wir noch die Regressionsgerade in den Scatterplot ein. 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'>
_images/e66ec68a36783b4a3dc625b06017eba99d359f40ce266ebec6bfae194f66b1d7.png

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!

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

\[\begin{align*} \xi_i := \frac{1}{t_i - t_{i-1}} \log{\left( \frac{S_{t_i}}{S_{t_{i-1}}}\right)} = \frac{1}{t_i - t_{i-1}} r_{t_{i-1},t_i} \end{align*}\]

Dann muss allerdings auch der Gesamt-Return von \(t_0\) nach \(t_n\) annualisiert werden

\[\begin{align*} \xi_{total} &:= \frac{1}{t_n - t_{0}} \log{\left( \frac{S_{t_n}}{S_{t_{0}}}\right)} = \frac{1}{t_n - t_{0}} r_{t_0,t_n}\\ &= \frac{1}{t_n - t_{0}} \sum\limits_{i=1}^n r_{t_{i-1},t_i} \end{align*}\]

Für 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.000937
GOOGL    0.001383
MSFT     0.001081
TSLA     0.001924
dtype: float64: 
 
geometric mean daily R Ticker
AAPL     0.000784
GOOGL    0.001203
MSFT     0.000982
TSLA     0.001140
dtype: float64: 

check: geo mean Rendite = exp(mean daily log)-1 = Ticker
AAPL     0.000784
GOOGL    0.001203
MSFT     0.000982
TSLA     0.001140
dtype: float64 

Daily log-returns Ticker
AAPL     0.000784
GOOGL    0.001202
MSFT     0.000982
TSLA     0.001139
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.197538
GOOGL    0.303024
MSFT     0.247417
TSLA     0.287081
dtype: float64: 
 
mu_tilde year (Renditen R) Ticker
AAPL     0.266086
GOOGL    0.416608
MSFT     0.312890
TSLA     0.623103
dtype: float64: 
 
Approximation1 : mu_tilde_year = mu_tilde * trading days Ticker
AAPL     0.236041
GOOGL    0.348506
MSFT     0.272378
TSLA     0.484805
dtype: float64
 
Approximation2: exp(mu_year) -1  Ticker
AAPL     0.218400
GOOGL    0.353947
MSFT     0.280713
TSLA     0.332532
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.

Nach Formel annu-log-return – TO DO!!!!!

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                                              
2023-10-02       NaN       NaN       NaN       NaN
2023-10-03 -0.007800 -0.013054 -0.026482 -0.020357
2023-10-04  0.007282  0.020997  0.017617  0.057650
2023-10-05  0.007172 -0.001258  0.001253 -0.004259
2023-10-06  0.014643  0.018413  0.024436  0.001844
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: 729, Jahre: 1.9972602739726026
Ticker          AAPL     GOOGL      MSFT       TSLA
Date                                               
2023-10-02       NaN       NaN       NaN        NaN
2023-10-03 -2.847065 -4.764571 -9.665806  -7.430255
2023-10-04  2.657920  7.663918  6.430341  21.042095
2023-10-05  2.617902 -0.459194  0.457358  -1.554676
2023-10-06  5.344596  6.720585  8.919164   0.673111
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.321473
GOOGL    0.364435
MSFT     0.347146
TSLA     0.292175
dtype: float64

Kovarianzen annualisieren…

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.129382  0.069163  0.053071  0.168935
GOOGL   0.069163  0.190176  0.064896  0.174167
MSFT    0.053071  0.064896  0.088196  0.116333
TSLA    0.168935  0.174167  0.116333  1.293985
Approx1: Annualized covarince R (percentege changes): Sigma pprox Sigma_tilde
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.076643  0.037443  0.030873  0.077624
GOOGL   0.037443  0.090755  0.034098  0.072578
MSFT    0.030873  0.034098  0.049653  0.052475
TSLA    0.077624  0.072578  0.052475  0.393586
Approx2: Annualized covarince R (percentege changes): yearly cov = daily cov * 252
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.077779  0.037928  0.031494  0.079241
GOOGL   0.037928  0.090808  0.034385  0.073269
MSFT    0.031494  0.034385  0.050014  0.053319
TSLA    0.079241  0.073269  0.053319  0.401422
Annualized covariance r (log retunrs)
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.076643  0.037443  0.030873  0.077624
GOOGL   0.037443  0.090755  0.034098  0.072578
MSFT    0.030873  0.034098  0.049653  0.052475
TSLA    0.077624  0.072578  0.052475  0.393586
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.197538
GOOGL    0.303024
MSFT     0.247417
TSLA     0.287081
dtype: float64
Ticker
AAPL     0.197538
GOOGL    0.303024
MSFT     0.247417
TSLA     0.287081
dtype: float64
Ticker       AAPL      GOOGL       MSFT       TSLA
Ticker                                            
AAPL    19.314064   9.435756   7.780101  19.561221
GOOGL    9.435756  22.870303   8.592807  18.289636
MSFT     7.780101   8.592807  12.512652  13.223608
TSLA    19.561221  18.289636  13.223608  99.183583
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.076643  0.037443  0.030873  0.077624
GOOGL   0.037443  0.090755  0.034098  0.072578
MSFT    0.030873  0.034098  0.049653  0.052475
TSLA    0.077624  0.072578  0.052475  0.393586

Fazit: wir sehen dass die Skalierung der Log-Returns zwar zum exakten Erwartungswert-Vektor führt, aber die Kovarianz deutlich überschätzt, denn \(\mathrm{E} (X_1 + X_2 + \ldots + X_n ) = \mathrm{E} (nX_1)\), aber \(var(nX_1)=n^2 Var(X_1) \neq n Var(X_1)= var(X1+\ldots+X_n)\) to do – sauber formulieren!!!

Portfolio-Rendite#

Aus den obigen 4 Aktien stellen wir jetzt ein Portfolio zusammen. und investieren 1000$ zu 10% in 1. Aktie (AAPL), 30% in 2. Aktie (GOOGL), 40% in 3. Aktie (MSFT) und die verbleibenden 20% in 4.Aktie (TSLA) und schichten das Portfolio im Folgenden nicht mehr um

weights = np.array([0.1, 0.3, 0.4, 0.2])
print("Summe der Portfoliogewichte = %f" % weights.sum())

startkapital = 1000
S0 = close_df.iloc[0]
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) )

portfolio = close_df.copy()
print(portfolio.head())
print("Type: %s" % type(portfolio))
Summe der Portfoliogewichte = 1.000000
kaufen in t=0 Stückzahl Ticker
AAPL     0.581177
GOOGL    2.251391
MSFT     1.261740
TSLA     0.794913
Name: 2023-10-02 00:00:00, dtype: float64 

 zum Preis Ticker
AAPL     172.064651
GOOGL    133.250961
MSFT     317.022461
TSLA     251.600006
Name: 2023-10-02 00:00:00, dtype: float64 

 für insgesamt  1000.0 Dollar
Ticker            AAPL       GOOGL        MSFT        TSLA
Date                                                      
2023-10-02  172.064651  133.250961  317.022461  251.600006
2023-10-03  170.727737  131.522858  308.737366  246.529999
2023-10-04  171.975510  134.313644  314.224701  261.160004
2023-10-05  173.213409  134.144775  314.618683  260.049988
2023-10-06  175.768387  136.637604  322.401428  260.529999
Type: <class 'pandas.core.frame.DataFrame'>

Damit können wir jetzt natürlich auch den Wert des Portfolios zu jedem späteren Zeitpunkt berechnen. Wird in \(t=0\) das Portfolio aus jeweils \(a_i\) Stocks zum Preis \(S_{i,0}\) zusammengesetzt (\(i=1, \ldots,n\)), gilt für den jeweiligen Portfolioanteil der \(i\)-ten Aktie \(w_i = \frac{a_i S_{i,0}}{P_0}\) und \(\sum\limits_{i=1}^n w_i = 1\), wobei \(P_0 = a_1 S_{1,0} + \ldots + a_n S_{n,0}\) der investierte Gesamtbetrag beträgt. Wird das Portfolio bis zum Zeitpunkt \(t>0\) nicht umgeschichtet, hat das Portfolio dann den Wert \(P_t = \sum\limits_{i=1}^n a_i S_{i,t}\). 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).

#Umwandeln in Numpy-Array, um einfacher zu rechnen
stueckzahl_np = stueckzahl.to_numpy()
print(stueckzahl_np)

portfolio['P_t'] = portfolio.dot(stueckzahl_np)

#to do alternative Berechnung?? 

print(portfolio.head())
[0.5811769  2.25139089 1.26174025 0.79491254]
Ticker            AAPL       GOOGL        MSFT        TSLA          P_t
Date                                                                   
2023-10-02  172.064651  133.250961  317.022461  251.600006  1000.000000
2023-10-03  170.727737  131.522858  308.737366  246.529999   980.848529
2023-10-04  171.975510  134.313644  314.224701  261.160004  1006.410023
2023-10-05  173.213409  134.144775  314.618683  260.049988  1006.364009
2023-10-06  175.768387  136.637604  322.401428  260.529999  1023.662604
portfolio.plot()
<Axes: xlabel='Date'>
_images/e9f90d6b4ff8f92cec87dda0ad4a9b3a80e741df929f76f4e4124f341044e778.png

Die prozentualen Potrtfolio-Renditen ergeben sich als gewichtete Summe der Aktienrenditen:

Für die Portfolio-Rendite, also den prozentualen Zuwachs bis zum Zeitpunkt \(t\) des Portfolios gilt dann wegen

()#\[\begin{split}\begin{aligned} R^P_t &= \frac{P_t}{P_0}-1 = \sum\limits_{i=1}^n a_i \frac{S_{i,t}}{P_0} - 1 = \sum\limits_{i=1}^n a_i \frac{S_{i,0}}{P_0} \frac{S_{i,t}}{S_{i,0}} - 1 \\ &= \sum\limits_{i=1}^n w_i \left(\frac{S_{i,t}}{S_{i,0}} - 1 \right) = \sum\limits_{i=1}^n w_i R_{i,t} = R_t^T \cdot w, \end{aligned}\end{split}\]

d. h. die Portfolio-Rendite ergibt sich als die gewichtete Summe der Aktien-Renditen \(R_{i,t}=\frac{S_{i,t}}{S_{i,0}} - 1\) und kann bequem mittels Vektoren \(R_t = (R_{1,t}, \ldots, R_{n,t})^T\) und Gewichtsvektor \(\omega = (\omega_1, \ldots, \omega_n)^T\) auch als Matrix-Vektor Produkt geschrieben werden. Für die Log-Returns gilt dies nur näherungsweise, allerdings können wir diese einfach mit der Beziehung () berechnen.

#daily returns / taegliche Renditen von t-1, t
#aktien_renditen = close_df.pct_change()
#print(aktien_renditen.head())

renditen_tag = portfolio.pct_change()
renditen_tag.plot()
print(renditen_tag.head())
Ticker          AAPL     GOOGL      MSFT      TSLA       P_t
Date                                                        
2023-10-02       NaN       NaN       NaN       NaN       NaN
2023-10-03 -0.007770 -0.012969 -0.026134 -0.020151 -0.019151
2023-10-04  0.007309  0.021219  0.017773  0.059344  0.026061
2023-10-05  0.007198 -0.001257  0.001254 -0.004250 -0.000046
2023-10-06  0.014750  0.018583  0.024737  0.001846  0.017189
_images/5186ed6ac695e9e1fc7e4fbeadacdfe702c05af9a615aa879939c7da6e13af77.png
#kumulierte Renditen von 0..t
renditen_kum=(1+renditen_tag).cumprod()-1
#haengen 0 an, da Portfoliogewicht, um die letzte Spalte (Portfoliowert) nicht zu beruecksichtigen
weights0 = np.append(weights,0)
print(weights0)

renditen_kum["check"] = renditen_kum.dot(weights0)
renditen_kum["PLog"] = np.log(renditen_kum.P_t+1)

print(renditen_kum.tail())
[0.1 0.3 0.4 0.2 0. ]
Ticker          AAPL     GOOGL      MSFT      TSLA       P_t     check  \
Date                                                                     
2025-09-24  0.466367  0.854696  0.609192  0.759897  0.698702  0.698702   
2025-09-25  0.492869  0.844565  0.599350  0.682790  0.678954  0.678954   
2025-09-26  0.484675  0.850193  0.613324  0.750397  0.698934  0.698934   
2025-09-29  0.478688  0.831507  0.623229  0.761566  0.698925  0.698925   
2025-09-30  0.479851  0.824377  0.633796  0.767568  0.702330  0.702330   

Ticker          PLog  
Date                  
2025-09-24  0.529864  
2025-09-25  0.518171  
2025-09-26  0.530001  
2025-09-29  0.529996  
2025-09-30  0.531998  

Wir sehen dass die beiden Spalten ‘P_t’, bei dem die Zuwachsraten von 0 bis t direkt aus den Portfoliowerten, und die Spalte ‘check’, bei dem die kumulierten Portfolio-Renditen als gewichtete Summe wie in Formel () berechnet wurden, tatsächlich übereinstimmen. 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.drop(columns="check").plot()
<Axes: xlabel='Date'>
_images/f1cda2a88595d99dea364221738d3222043fa1d6dfb4412fa1c1fb870d3d7ece.png

Portfolioanalyse: erwartete Portfoliorendite und Portfoliovarianz#

Wegen Formel () können wir die erwartete Rendite sowie die Portfolio-Varianz einfach aus dem Zufalls-Vektor \(R_t = (R_{1,t}, \ldots, R_{n,t})^T\) der Aktienrrenditen[8] und seinen ersten beiden Momenten \(\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

\[\begin{align*} \mathbb{E} R_t^P &= \mathbb{E} \left( R_t^T \cdot w \right) = \left( \mathbb{E} R_t^T \right) \cdot w = \mu_t^T \cdot w \\ \operatorname{Var} (R_t^P) &= \mathbb{E} \left( \left( R_t^T \cdot w - \mathbb{E}(R_t^T \cdot w) \right)^T \left( R_t^T \cdot w - \mathbb{E}(R_t^T \cdot w) \right) \right)\\ &= w^T \left\lbrace \mathbb{E} \left( R_t - \mathbb{E} R_t \right) \left(R_t - \mathbb{E} R_t \right)^T \right\rbrace w\\ &= w^T \Sigma_t w \end{align*}\]

to-do: check!!!!#

Ü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. Für die Renditen geht man ähnlich vor und 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 nicht so deutlich sind und daher der Approximationsfehler in Formel eq:rendite-annualized-approx nicht wesentlich ausfällt.

#to do codebeispiel
#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.000937
GOOGL    0.001383
MSFT     0.001081
TSLA     0.001924
dtype: float64: 
 
geometric mean daily R Ticker
AAPL     0.000784
GOOGL    0.001203
MSFT     0.000982
TSLA     0.001140
dtype: float64: 

check: geo mean Rendite = exp(mean daily log)-1 = Ticker
AAPL     0.000784
GOOGL    0.001203
MSFT     0.000982
TSLA     0.001140
dtype: float64 

Daily log-returns Ticker
AAPL     0.000784
GOOGL    0.001202
MSFT     0.000982
TSLA     0.001139
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.

#annualization: from daily means to yearly means
trading_days = 252

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.197538
GOOGL    0.303024
MSFT     0.247417
TSLA     0.287081
dtype: float64: 
 
mu_tilde year (Renditen R) Ticker
AAPL     0.266086
GOOGL    0.416608
MSFT     0.312890
TSLA     0.623103
dtype: float64: 
 
Approximation1 : mu_tilde_year = mu_tilde * trading days Ticker
AAPL     0.236041
GOOGL    0.348506
MSFT     0.272378
TSLA     0.484805
dtype: float64
 
Approximation2: exp(mu_year) -1  Ticker
AAPL     0.218400
GOOGL    0.353947
MSFT     0.280713
TSLA     0.332532
dtype: float64: 

Die Auswirkungen auf die erwartete jährliche Portfoliorendite je nach Berechnungsart:

print("erwartete annualisierte Portfoliorendite: \n"
"  %f (mu_tilde_year -- correct) \n"
"  %f (mu_year) \n"
"  %f (Approx1) \n"
"  %f (Approx2) \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.401368 (mu_tilde_year -- correct) 
  0.267044 (mu_year) 
  0.334068 (Approx1) 
  0.306816 (Approx2) 

Obwohl die 2. Approximation bessere Ergebnisse liefert, werden wir aus Gründen der Einfachheit die simplere 1. Approximation für die Berechnung der Kovarianzmatrix der Jahres-Renditen \(\tilde{\Sigma}_\text{annualized} \approx 252 \cdot \tilde{\Sigma}_text{daily}\) verwenden.

to do!!!

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.129382  0.069163  0.053071  0.168935
GOOGL   0.069163  0.190176  0.064896  0.174167
MSFT    0.053071  0.064896  0.088196  0.116333
TSLA    0.168935  0.174167  0.116333  1.293985
Approx1: Annualized covarince R (percentege changes): Sigma pprox Sigma_tilde
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.076643  0.037443  0.030873  0.077624
GOOGL   0.037443  0.090755  0.034098  0.072578
MSFT    0.030873  0.034098  0.049653  0.052475
TSLA    0.077624  0.072578  0.052475  0.393586
Approx2: Annualized covarince R (percentege changes): yearly cov = daily cov * 252
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.077779  0.037928  0.031494  0.079241
GOOGL   0.037928  0.090808  0.034385  0.073269
MSFT    0.031494  0.034385  0.050014  0.053319
TSLA    0.079241  0.073269  0.053319  0.401422
Annualized covariance r (log retunrs)
Ticker      AAPL     GOOGL      MSFT      TSLA
Ticker                                        
AAPL    0.076643  0.037443  0.030873  0.077624
GOOGL   0.037443  0.090755  0.034098  0.072578
MSFT    0.030873  0.034098  0.049653  0.052475
TSLA    0.077624  0.072578  0.052475  0.393586

Portvoliovarianz exakt berechnet:

P_var_year = (Sigma_tilde_year.dot(weights)).dot(weights)
print(P_var_year)

#alternative Berechnung mittels matmult
print(np.matmul(weights.transpose(), np.matmul(Sigma_tilde_year, weights)))
0.15452171523599625
0.15452171523599625
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("annualisierte Portfolio-Varianz: \n"
"  %f (korrekt) \n"
"  %f (Approx1) \n"
"  %f (Approx2) \n" % 
      (P_var_year, P_var_year_approx1, P_var_year_approx2 ))
annualisierte Portfolio-Varianz: 
  0.154522 (korrekt) 
  0.065733 (Approx1) 
  0.066550 (Approx2)