Backtest gleitender Durchschnitte in Python

Python ist eine der meistgenutzten Programmiersprachen weltweit und daher auch in der Finanzwelt weit verbreitet. Der Grund hierfür ist die Einfachheit, die damit verbundene steile Lernkurze sowie die vielen nützlichen libraries, die es Privatianlegern ermöglichen, selbst komplexe Analysen schnell und einfach umzusetzen. In diesem Artikel werde ich eine einfache Strategie, die Kreuzung gleitender Durchschnitte, inklusive dem dazu notwendigen Code in Python, erläutern.

Die Kreuzung gleitender Durchschnitte ist eine extrem bekannte, vereinfachende Momentum-Strategie und wird oft als das “Hello World”-Beispiel für den quantitativen Handel angesehen.

Was ist eigentlich ein gleitender Durchschnitt?

Ein gleitender Durchschnitt, auch gleitender oder laufender Mittelwert genannt, wird verwendet, um Zeitreihendaten zu analysieren, indem Mittelwerte verschiedener Teilmengen des gesamten Datensatzes berechnet werden. Da es sich um den Durchschnitt von Datenwerten im Zeitverlauf handelt, wird er auch als gleitender oder rollender Mittelwert bezeichnet.

Strategie

Die im Folgenden verwendete Strategie ist Long-only. Es werden zwei separate, einfache gleitende Durchschnitte mit unterschiedlichen Rückblickperioden einer bestimmten Zeitreihe berechnet. Kaufsignale treten auf, wenn der kürzere gleitende Durchschnitt den längeren gleitenden Durchschnitt übersteigt. Wenn der längere Durchschnitt anschließend den kürzeren Durchschnitt übersteigt, wird ein Verkaufsignal generiert. Die Strategie funktioniert vor allem dann gut, wenn in der Zeitreihe eine starke Trendperiode eintritt und sich der Trend dann langsam umkehrt. In diesem Artikel wird ein einfacher gleitender Durchschnitt verwendet, welcher die Daten der gesamten Rückblickperiode gleich gewichtet.

Welches Underlying?

Für dieses Beispiel habe ich Apple, Inc. (AAPL) als Zeitreihe gewählt, mit einem kurzen gleitenden Durchschnitt von 50 Tagen und einem langen gleitenden Durchschnitt von 200 Tagen.

Los geht's in Python

Falls du Python bereits installiert hast und grob weißt, wie man ein Jupyter Notebook verwendet, dann kannst du alles einfach nachprogrammieren. Falls du noch keine Erfahrung mit dem Programm hast und mehr über erste Schritte in Python erfahren möchtest, schreib mir gern in die Kommentare!

Der Datenprovider für das nachfolgende Beispiel ist Quandl. Dort findet man jede Menge kostenlos verfügbare Finanzdaten sowie fortgeschrittenere Datensätze (bspw. über Futures und Optionen) zu einem akzeptablen Preis.

#Import aller benötigten Libraries
import pandas as pd
import numpy as np
import quandl
import matplotlib.pyplot as plt  
import seaborn as sns

#Für den Zugriff auf Quandl benötigst Du einen eigenen API Key
quandl.ApiConfig.api_key = "#######"

#Laden der historischen Daten
aapl = quandl.get('EOD/AAPL', start_date='2010-01-01', end_date='2021-01-01')

Überblick über die Daten

Die Zeitreihe startet am 04. Januar 2010 und reicht bis zum 31. Dezember 2020. Sie enthält neben der reinen Preisreihe auch Daten über das gehandelte Volumen, Dividenden und Aktiensplits. Für diesen Artikel sind allerdings einzig die Preisdaten relevant, welche im Datensatz als “Adjusted_Close” angegeben sind. In dieser Variable sind bereits gezahlte Dividenden und Aktiensplits bereits berücksichtigt.

#Der Befehl .head() gibt die obersten 5 Reihen des Datensatzes aus
aapl.head()

Buy and Hold

In einem ersten Schritt berechne ich die täglichen Renditen der Aktie. So lässt sich herausfinden, wie eine einfache Buy and Hold Strategie über die letzten zehn Jahre performt hätte.

#Renditen berechnen
aapl['rets'] = aapl['Adj_Close'].pct_change()

#Plotten der Renditen
plt.plot(aapl.rets,linewidth = 1)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show()

Die obige Grafik zeigt alle täglichen Renditen von Apple in den letzten elf Jahren. Gut zu sehen sind bspw. die hohen negativen täglichen Renditen während der Korrektur des Lockdowns im Frühjahr 2020.

Auf Basis dieser täglichen Renditen ist es nun möglich, die kumulierte Performance einer einfachen Investition über 1 USD zu berechnen. (Im Fall von Apple ist dies eine vereinfachende Annahme, da in der Praxis ein Kauf über 1 USD nur über den Erwerb von Teilaktien möglich wäre.)

#Berechnen der kumulierten Performance
aapl['perc_ret'] = (1 + aapl.rets).cumprod()

#Plotten der Performance
plt.plot(aapl.perc_ret, linewidth = 3)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show()

Beeindruckender Anstieg in den letzten elf Jahren, nicht wahr? Aus einem investierten USD Anfang 2010 wurden bis Ende 2020 über 20 USD! Auffallend ist außerdem der steile Anstieg im Jahr 2020 nach dem ersten Lockdown. Ausgehend vom Tiefpunkt im Frühjahr 2020 hat sich das Investment fast verdreifacht!

Im nächsten Schritt habe ich versucht, mittels einfacher gleitender Durchschnitte die Drawdowns zu umgehen so lediglich die steilen Anstiege des Aktienkurses “herauszuschneiden”.

Gleitende Durchschnitte - 50er und 200er

Die nachfolgende Strategie basiert, wie oben bereits angedeutet, auf den Krezungen beider gleitenden Durchschnitte. Sobald der 50er über dem 200er liegt, dann sind wir long investiert. Sollte der 50er dann unter den 200er fallen, liquidieren wir die Anlage und halten Cash. Ziel der Strategie ist es, die Drawdowns zu umgehen und nur an den Aufwärtstrends zu partizipieren.

Um gleitende Durchschnitte in Python zu berechnen, benötigt man die library “pandas”, die weiter oben im Code bereits geladen wurde. Pandas bietet jede Menge nützliche Funktionen im Bereich der Datenanalyse und ist wohl deshalb auch eine der meistgenutzten libraries. (In einer library sind einige Funktionen bereits vorprogrammiert, die dann einfach per Befehl benutzt werden können. So muss man nicht alles selbst programmieren.)

#50er gleitender Durchschnitt
aapl['SMA_50'] = aapl.Adj_Close.rolling(50).mean()
#200er gleitender Durchschnitt
aapl['SMA_200'] = aapl.Adj_Close.rolling(200).mean()
#Plotten der Ergebnisse
plt.plot(aapl.Adj_Close,linewidth = 3, label = 'AAPL')
plt.plot(aapl.SMA_50,linewidth = 3, label ='SMA_50')
plt.plot(aapl.SMA_200,linewidth = 3,label='SMA_200')
plt.legend(loc='upper left')
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.legend()
plt.show()

In der oberen Grafik sind die Preisdaten der Apple Aktie (blau) sowie die beiden gleitenden Durschnitte abgetragen. Man erkennt leicht, dass die Datenwerte der Apple Aktie fast zu jedem Zeitpunkt höher lagen als der gleitenden Durschnitte der letzten 50 bzw. 200 Tage.

Im nächsten Schritt werden die Kreuzungen der beiden gleitenden Durchschnitte und daraus die Performance dieser Strategie berechnet. In der nachfolgenden Grafik ist die kumulierte Performance eines theoretisch investierten USD abgetragen.

#Berechnen der Krezungen
aapl['position'] = np.where(aapl.SMA_50 > aapl.SMA_200, 1,0) * aapl.rets
#Kumulierte Performance
aapl['perf'] = (1 + aapl.position).cumprod()
#Plotten der Ergebnisse
plt.plot(aapl.perf,linewidth = 3)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show()

Beim Vergleich der Strategie gleitender Durchschnitte mit dem weiter oben beschriebenen Buy and Hold Investment ist leicht zu erkennen, dass die Strategie gleitender Durschnitte weitaus schlechter performt. Zwar gelingt es einige Drawdowns auszulassen, jedoch wird die Trendumkehr mit dem daraus resultierenden Anstieg verpasst.

Ein weiterer Versuch

In einem weiteren Vergleich beider Strategien betrachte ich nun nicht mehr die kumulierte Performance als Variable, sondern nutze die annualisierte Rendite sowie die annualisierte Standardabweichung.

#APPLE - Buy and Hold
#Gesamtrendite
Total_return_aapl = (aapl.Adj_Close[-1] - aapl.Adj_Close[0]) / aapl.Adj_Close[0] 
#Anzahl der Monate
D = (aapl.index.year[-1] - aapl.index.year[0]) * 12 
#Annualisierte Rendite
Ann_ret_aapl = (( 1 + Total_return_aapl) ** (12 / D))-1 

print(Ann_ret_aapl)
0.35013772345342264

#Strategie gleitender Durchschnitte
#Gesamtrendite
Tot_ret_strat = (aapl.perf[-1] - aapl.perf[1]) / aapl.perf[1]
#Annualisierte Rendite
Ann_ret_strat = (( 1 + Tot_ret_strat) ** (12 / D))-1 

print(Ann_ret_strat)
0.2686405093190718

Insgesamt weißt das Buy and Hold Investment eine um fast 9 Prozentpunkte höhere annualisierte Rendite auf als die Strategie gleitender Durschnitte. Dieses Ergebniss ist nach den Plots der kumulierten Performance weiter oben nicht wirklich überraschend.

Wichtig ist es außerdem, einen Blick auf das Risiko beider Strategien richten. Hierzu berechne ich die annualisierte Standardabweichung beider Strategien. (Hierfür ist die library “numpy” notwendig, die ganz oben im Code bereits geladen wurde.)

#APPLE
Std_aapl = np.std(aapl.rets) * np.sqrt(250)

print(Std_aapl)
0.28184954635180737

#Strategie
Std_strat = np.std(aapl.position) * np.sqrt(250)

print(Std_strat)
0.24077936253678048

Aus Risikogesichtspunkten schneidet die Strategie gleitender Durschnitte besser ab, da die Standardabweichung hier gringer ist.

Darüber hinaus könnte man noch weiterere Metriken, wie bspw. den maximalen Drawdown, die Länge des Drawdowns, die Sharpe Ratio o. Ä., berechnen und beide Strategien dahingehend vergleichen. Primäres Ziel dieses Blogartikels ist es jedoch nicht, beide Strategien umfassend zu vergleichen, weshalb ich hier nicht detaillierter einsteige. Vielmehr möchte ich euch ein paar Ansätze näherbringen, mit denen ihr historische Daten mit Python auswerten und so verschiedene Strategien selbst testen könnt.

Wie sieht es mit anderen gleitenden Durchschnitten aus?

Zuletzt habe ich mit dem Datensatz von oben sämtliche gleitenden Durchschnitte mit Rückblickperioden von 20 bis 200 Tagen (also 181 verschiedene) berechnet und alle möglichen Zweierkombinationen dieser 181 gleitenden Durchschnitte geprüft. Das sind insgesamt über 16000 Kombinationen! In Python ist diese Berechnung aber wenig rechenintensiv. Es wird lediglich eine Schleife (eine sogenannte loop) benötigt, die für jede Zweierkombination von gleitenden Durchschnitten die resultierende Performance berechnet.

#Berechnung der gleitenden Durchschnitte: 20 bis 200
for GD in range(20,201,1):
    aapl[GD] = aapl.Adj_Close.rolling(GD).mean()

#Kreuzungen der gleitenden Durchschnitte kennzeichnen
for short in range(20, 201):
    for long in range(short + 1, 201):
        aapl[short,long] = (1+ (np.where(aapl[short] > aapl[long],1,0)) * aapl['rets']).cumprod()

#Plotten der Ergebnisse
for short in range(20, 200):
    for long in range(short + 1, 200):
        plt.plot(aapl[short,long])
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show()

In der obigen Grafik sind die Performances aller Zweierkombinationen von gleitenden Durchschnitten abgetragen. Sie sieht jedoch eher aus wie ein Kunstwerk, da man vor lauter Farben nichts erkennen kann. Welche konkreten Zweierkombinationen besonders gut performt haben, lässt sich nur schwer ablesen.

Besonders interessant ist außerdem nicht nur die beste Zweierkombination, sondern die Betrachtung mehrerer gut funktionierender Kombinationen. Daraus kann man bspw. ableiten, ob tendenziell eher zwei kurzfristige, zwei langfristige oder die Kombination aus kurz- und langfristigen gleitenden Durchschnitte gut für die Strategie geeignet sind. Dies kann mit einer Heatmap gut visualisiert werden. Mit solch einer Heatmap wird der eine gleitende Durchschnitt auf der vertikalen und der andere auf der horizontalen Achse geplottet. Die Performance der jeweiligen Zweierkombination wird dann mittles eines Farbcodes dargestellt, sodass leicht erkennbar wird, welche Art der Zweierkombination zu bevorzugen ist.

rets = {}
ann_rets = {}

#Berechnen der Renditen
for short in range(20, 201):
    for long in range(short + 1, 201):
        
        #Gesamtrendite
        rets[short,long] = (aapl[short,long][-1] - aapl[short,long][1]) / aapl[short,long][1]
        
        #Annualisierte Rendite
        ann_rets[short,long] = (( 1 + rets[short,long]) ** (12 / D))-1 

#Formatierung
df1 = pd.DataFrame(ann_rets.values())
df2 = pd.DataFrame(list(ann_rets.keys()))
df = pd.concat([df2, df1], axis=1)
df.columns = ['SMA_1','SMA_2','Ann_rets']
print(df)

       SMA_1  SMA_2  Ann_rets
0         20     21  0.323400
1         20     22  0.327452
2         20     23  0.374134
3         20     24  0.345950
4         20     25  0.325199
...      ...    ...       ...
16285    197    199  0.278211
16286    197    200  0.283091
16287    198    199  0.284195
16288    198    200  0.286759
16289    199    200  0.287948

[16290 rows x 3 columns]

#Pivottabelle erstellen
pivot = df.pivot('SMA_1','SMA_2','Ann_rets')
#Plotten der Heatmap
ax = sns.heatmap(pivot, cmap="RdYlGn")
sns.set(font_scale=2)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.show()

In der obigen Grafik ist eine solche Heatmap dargestellt. Es lässt sich auf einen Blick erkennen, dass Kombinationen aus zwei längeren gleitenden Durchschnitten (grüner Bereich rechts unten) tendenziell eine höhere annualisierte Rendite liefern. Außerdem wird deutlich, dass Kombinationen aus zwei recht kurzen gleitenden Durchschnitten tendenziell eine niedrige annualisierte Rendite liefern (roter Bereich links bis zur Mitte). Dies liegt wohl an den ständigen Ein- und Ausstiegen, da eine kurzfristige Strategie stark auf Marktschwankungen reagiert. (In diesem Backtest bleiben Transaktionskosten unberücksichtigt, die sich auf kurzfristige Strategien zudem besonders negativ auswirken würden.) Interessant sind zudem Kombinationen, bei denen die Rückblickperioden beider gleitender Durchschnitte nur knapp über 20 betragen (grüner Bereich ganz links oben). Da der grüne Bereich jedoch sehr klein ist, ist eine solch kurzftistige Strategie wenig robust (bei einer kleinen Erhöhung der für die gleitenden Durchschnitte genutzten Zeiträume wäre man schnell im “roten” Bereich mit einer geringen annualisierten Rendite), sodass diese Kombinationen wohl nicht zu empfehlen sind.

Fazit

Das Ziel dieses Blogartikels ist es zu zeigen, wie man mit ein paar Zeilen Code verschiedenste Kombinationen der Strategie gleitender Durchschnitte backtesten und vergleichen kann. Für eine umfassende Analyse müssten jedoch zusätzliche Aspekte, wie bpsw. Transaktionskosten, Slippage oder Steuern, auf jeden Fall beachtet werden!

Falls Dir dieser Artikel inklusive des praktischen Ansatzes auf Basis des Python Codes gefallen hat, dann teil mir das doch bitte in den Kommentaren mit.

Viele Grüße

Felix von Portfolio-Architekt

Inhalte werden geladen

Dieser Beitrag hat 5 Kommentare

  1. Heiko

    Moin Felix,

    vielen Dank für deinen spannenden Blog. Ich freue mich auf mehr. Insbesondere an automatischen Handelsansätzen bin ich sehr interessiert. Nutzt du automatische Handelsansätze zur Vermehrung deines eigenen Vermögens?

    Gruß

    Heiko

    1. Felix

      Hallo Heiko,

      erstmal Danke für dein Lob!
      In der Tat nutze ich solche Ansätze. Sie betragen (aktuell) zwar nur einen kleinen Teil, allerdings bin ich ein großer Freund systematischer Ansätze. Bei diesen Strategien werden die Signale automatisch ausgewertet und die resultierende Allokation dann an mich übermittelt. Die Änderung im Portfolio nehme ich dann manuell vor.

      Viele Grüße
      Felix

  2. Georg

    Moin Felix,

    Danke für den Artikel und den Python Code. Irgendwann will ich mich damit auch mal beschäftigen, und ich lerne am schnellsten durch Beispiele. Kannst Du ein Buch zum Thema Python und Finanzanalyse empfehlen? Am besten mit ganz viel Beispielcode?

    Zum Thema Strategien könntest du mal die Turtle Trading Strategie aufsetzen. Auch wenn das System veraltet ist, wird damit gut beschrieben wie eine vollständige Strategie aussieht (Entry, Exit, Size, etc.). Von dem Code hätte ich dann auch gerne eine Kopie 🙂

    Schöne Grüße, Georg

    1. Felix

      Hallo Georg,

      das mit der Turtle Trading Strategie ist kein Problem 🙂
      Also deutsche Bücher die sich mit dem Thema Trading beschäftigen und dazu noch in Python kenne ich leider keine. Ein gutes Buch mit viel Code ist bspw. das von Andreas Clenow – trading evolved. Darin werden ein paar Strategien inklusive Code beschrieben.
      Die Strategie von Stocks on the Move von Andreas Clenow werde ich sowieso bald inklusive Code hier veröffentlichen. Das sollte dann definitiv in dein Interessenspektrum fallen! Und das gibt es übrigens mittlerweile auch auf deutsch!

      Viele Grüße
      Felix

Schreibe einen Kommentar