Der Gebert-Indikator ist ein Börsenindikator, der in der Lage sein soll, auf Basis weniger (vor allem makroökonomischer) Kennzahlen durch systematische Phasen des Kaufens / Verkaufens im DAX eine Überperformance zu erzielen. Hierzulande zählt dieser Indikator vermutlich zu den „Klassikern“ und oft wurden im Zusammenhang mit diesem Indikator exponentiell in die Höhe schießende Kapitalkurven gezeigt, die ihre beeindruckende Form vermutlich aber dem Zinseszinseffekt in sehr langfristigen Backtests zu verdanken haben. Wir zeigen, wie der Indikator berechnet wird, wie man eine kleine Shiny-App entwerfen könnte, die den Stand des Indikators automatisch aktualisiert, und wie eine Simulation der Performance des Indikators aussieht.
Web-App mit aktuellem Stand des Gebert-Indikators
Wir haben eine kleine Web-App entwickelt, die den Stand des Gebert-Indikators und der dazugehörigen makroökonomischen Variablen darstellt – täglich automatisch aktualisiert.
Der Stand des Gebert-Indikators ist im oberen Diagramm dargestellt:
- „Gebert“ ist der aktuelle Punktestand (Details siehe unten)
- „Signal“ ist die aktuelle Positionierung (also investiert oder in Cash, Details siehe ebenfalls unten)
An der linken Seite finden sich die makroökonomischen Variablen, die in die Berechnung des Indikators eingehen, und der abgeleitete Punktwert.
Der Algorithmus des Gebert-Indikators
Es wird monatlich ein Punktestand zwischen 0 und 4 berechnet:
- Ein Punkt, wenn der letzte Zinsschritt der EZB eine Senkung war, andernfalls 0 Punkte.
- Ein Punkt, wenn die Inflation in der Eurozone unterhalb derer des Vorjahresmonats liegt, andernfalls 0 Punkte
- Ein Punkt, wenn EUR/USD im Vergleich zum Vorjahresmonat gefallen ist, andernfalls 0.
- Ein zusätzlicher Punkt im Zeitraum von November bis April.
Bei einem Punktestand von…
- 0 oder 1: Verkaufen oder weiterhin nicht investieren.
- 3 oder 4: Kaufen oder weiterhin investiert bleiben.
- 2: den Punktestand des Vormonats beibehalten.
Backtest
Nun zum spannenden Teil: Funktioniert der Gebert-Indikator überhaupt (sofern man bei derartigen Systemen von „funktionieren“ sprechen kann)?
Im Backtest von 2002 bis 2021 produziert der Gebert-Indikator tatsächlich eine leicht bessere risikoadjustierte Performance als ein Buy-and-Hold des DAX. Zwar ist eine Überrendite nicht vorhanden (oder die Renditedifferenz sogar leicht negativ), aber der Indikator vermied wie auch schon in der Vergangenheit größere Drawdowns. So liegt der maximale Drawdown des DAX in diesem Zeitraum bei ca. 50%, beim Gebert-System bei „nur“ ca. 30 %. Auch zwei weitere, kleine Drawdowns fielen geringer aus. Die Sharpe Ratio steigt von 0,4 auf 0,56. Den Corona-Crash hat das System nicht vermeiden können – was selbstverständlich auch etwas viel verlangt wäre.
Gebert-Indikator in der Praxis
Was fängt man nun mit diesem Ergebnis in der Praxis an? Nicht unerwähnt bleiben sollte, dass der obige Backtest noch keine Transaktionskosten enthält. Das System tätigt jedoch nur wenige Umsätze, so dass die Transaktionskosten nicht allzu sehr ins Gewicht fallen werden, wenn man die Orders halbwegs geschickt erteilt. Wir nutzen für derartige Systeme in der Regel MOC-Orders bei Interactive Brokers.
Eventuelle steuerliche Implikationen durch die häufiger realisierten Gewinne sollten ebenfalls bedacht werden. Dadurch, dass Gewinne erst zum Ende des Anlagezeitraums realisiert werden, bietet das Buy-and-Hold-System den Vorteil eines Steuerstundungseffekts.
In den letzten knapp 20 Jahren jedenfalls wurde keine Überrendite, sondern „nur“ eine höhere Sharpe Ratio erzielt und zudem schwindet jedes „Alpha“ mit der Zeit. Bei ausreichend langem Anlagehorizont ist es meist die beste Strategie, Aktien zu halten „und Schlaftabletten zu nehmen“, wie Kostolany gesagt hätte. Schließlich läuft man hier Gefahr, nicht den nächsten Crash zu vermeiden, sondern die nächste Rally zu versäumen – die Kehrseite der Medaille.
Natürlich muss die Geschichte hier nicht enden und wie erwähnt scheint der Indikator nach wie vor eine gewisse Vorhersagekraft zu besitzen. Beispielsweise könnten der Indikator und seine Variablen nicht nur isoliert, sondern auch als Teil eines größeren Systems genutzt werden.
Newsletter
Technischer Hintergrund: Umsetzung des Gebert-Indikators in R
Für unsere Web-App benötigen wir Funktionen, die für uns folgende Aufgaben erledigen:
- Laden der Daten (z. B. von Yahoo-Finance und DBnomics).
- Angleichen der Zeitreihen. Zeitreihendaten aus „freier Wildbahn“ weisen meist Lücken oder fehlerhafte Daten auf.
- Berechnung des Indikators.
- Einen Backtest (Simulation hypothetischer Wertentwicklung in der Vergangenheit) durchführen.
Mittlerweile entsteht durch Pakete wie dplyr, tidyquant und tidyr oft recht eleganter und gut lesbarer Code, auch wenn beispielsweise xts nach wie vor seine Daseinsberechtigung hat und auch hier wieder zum Einsatz kommt. Jedenfalls ist der Code kompakt genug, um ihn hier fast vollständig darstellen zu können.
Zuerst laden wir die neusten Daten. Fragen zum Code gerne in die Kommentare 👇
# Dependencies ========================
library(shiny)
library(shinycssloaders)
library(DT)
library(lubridate)
library(plotly)
library(tidyverse)
library(tidyquant)
library(rdbnomics)
library(PerformanceAnalytics)
# Load Data ----------------------------------------
# Zinsen
interest <- rdb(ids = 'ECB/FM/B.U2.EUR.4F.KR.DFR.CHG')
interest_t <- interest %>%
select(period, value) %>%
rename(date = period, interest = value) %>%
mutate(d_interest = interest - lag(interest))
newest_interest <- tail(interest_t$date, 1)
# Inflation
# Germany - HICP - All-items excluding energy and food,
# Annual rate of change, Eurostat, Neither seasonally
# nor working day adjusted, percentage change
inflation <- rdb(ids = 'ECB/ICP/M.DE.N.XEF000.4.ANR')
inflation_t <- inflation %>%
select(value, period) %>%
as_tibble() %>%
rename(date = period, inflation = value)
newest_inflation <- tail(inflation_t$date, 1)
# EURUSD
eurusd <- rdb(ids = 'BDF/EXR/EXR.D.USD.EUR.SP00.A')
eurusd_t <- eurusd %>%
select(value, period) %>%
as_tibble() %>%
rename(eurusd = value, date = period)
newest_eurusd <- tail(eurusd_t$date, 1)
# DAX
# dax_d.csv ist eine Tabelle mit älteren Kursen
dax_history <- read_csv("dax_d.csv")
dax_history_t <- dax_history %>%
select(Close, Date) %>%
as_tibble() %>%
rename(dax = Close, date = Date)
dax <- tq_get("^GDAXI")
dax_t <- dax %>%
select(close, date) %>%
as_tibble() %>%
rename(dax = close)
dax2 <- dax_history_t %>%
filter(!(date %in% dax_t$date)) %>%
bind_rows(dax_t) %>%
arrange(date)
newest_dax <- tail(dax2$date, 1)
Code-Sprache: PHP (php)
Angleichen der Zeitreihen
Um die einzelnen Zeitreihen in einer tibble zusammenzufassen, führen wir einen Join auf Basis der Datumsangaben durch. Hier entstehen nun Lücken in den Daten durch einzelne, fehlende Tage in den Zeitreihen. Diese Lücken füllen wir mit dem vorangehenden Wert. Anschließend wandeln wir die Daten zu monatlichen Daten um, da der Backtest nur auf Monatsbasis berechnet werden soll. Abschließend berechnen wir die 12-Monats-Differenzen, die wir für EUR/USD und die Inflation benötigen.
# Merge Data ----------------------------------------
dat <- full_join(dax2, interest_t) %>%
full_join(inflation_t) %>%
full_join(eurusd_t) %>%
arrange(date) %>%
fill(!date, .direction = "down") %>%
filter(date >= "2001-06-01") %>%
tq_transmute(select = c(dax, interest, d_interest, inflation, eurusd),
mutate_fun = to.period,
period = "months") %>%
pivot_longer(!date, values_to = "close") %>%
group_by(name) %>%
mutate(monthly_diff = close - lag(close),
yearly_diff = close - lag(close, n = 12)) %>%
drop_na()
Code-Sprache: PHP (php)
Berechnung des Signals
Der gute, alte For-Loop: An dieser Stelle möchten wir chronologisch die Indikatorwerte durchgehen und wie oben dargestellt das vorige Signal beibehalten, falls der Indikator auf 2 steht.
Zuerst berechnen wir jedoch die Punktwerte der einzelnen Variablen, also für die Saison (= den Monat), EUR/USD, Inflation und Zinsen.
dat_signal <- dat %>%
group_by(date) %>%
pivot_wider(names_from = name, values_from = close:yearly_diff) %>%
mutate(month_nr = month(date)) %>%
summarise(
month_indi = ifelse(month_nr %in% c(11, 12, 1, 2, 3, 4),
yes = 1, no = 0),
eurusd_indi = ifelse(yearly_diff_eurusd < 0, 1, 0),
inflation_indi = ifelse(yearly_diff_inflation < 0, 1, 0),
interest_indi = ifelse(close_d_interest <= 0, 1, 0),
close_dax = close_dax,
monthly_diff_dax = monthly_diff_dax,
gebert = month_indi + eurusd_indi + inflation_indi + interest_indi
) %>%
drop_na()
dat_signal <- dat_signal %>%
mutate(signal = NA)
dat_signal$signal[1] <- 1
for (i in 2:nrow(dat_signal)) {
if (dat_signal$gebert[i] >= 3) {
dat_signal$signal[i] <- 1
} else if (dat_signal$gebert[i] <= 1) {
dat_signal$signal[i] <- 0
} else {
dat_signal$signal[i] <- dat_signal$signal[i - 1]
}
}
Code-Sprache: PHP (php)
Ergebnis und Grafiken
Das war schon alles! Abschließend können wir nun noch den Backtest berechnen, wofür wir das Paket PerformanceAnalytics benutzen.
dat_signal <- dat_signal %>%
mutate(strategy_returns = monthly_diff_dax * signal,
strategy = cumsum(strategy_returns))
ggplot(dat_signal, aes(x = date, y = strategy)) +
geom_line() +
scale_x_date(date_breaks = "6 months") +
theme(axis.text.x = element_text(angle = 90))
dat_signal_xts <- xts(dat_signal$close_dax, order.by = dat_signal$date)
colnames(dat_signal_xts) <- "close_dax"
dat_signal_xts$returns_dax <- Return.calculate(dat_signal_xts)
dat_signal_xts$gebert_strategy <- dat_signal_xts$returns_dax * dat_signal$signal
dat_signal_xts <- dat_signal_xts[, c("returns_dax", "gebert_strategy")]
charts.PerformanceSummary(na.omit(dat_signal_xts),
main="Performance Summary",
geometric=FALSE, wealth.index=TRUE)
Code-Sprache: PHP (php)