Performance-Kennzahlen als wiederverwendbare Funktionen: CAGR, Drawdown, Sharpe, Sortino, Profit-Faktor und ihre Unit-Tests
Schreiben Sie ein kleines Kennzahlenmodul mit korrekter Annualisierung, ergänzen Sie rollierende Versionen und belegen Sie die Funktionen mit pytest anhand von Werten, die Sie von Hand berechnet haben.
Was Sie lernen werden
- Wie Sie jede Kennzahl als reine Funktion schreiben, die ein pandas-Objekt entgegennimmt und eine Zahl zurückgibt.
- Wo die Annualisierung ins Spiel kommt: die Wurzel-aus-der-Zeit-Regel für die Volatilität und die Konstante für Perioden pro Jahr.
- Wie rollierende Versionen zeigen, wann eine Strategie funktioniert hat und wann nicht.
- Wie Sie eine Kennzahl per Unit-Test gegen einen von Hand berechneten Wert prüfen, damit ein späteres Refactoring sie nicht unbemerkt beschädigen kann.
Erklärung
Die Definitionen von CAGR, maximalem Drawdown, Sharpe Ratio und Profit-Faktor wurden in „Systematisches Trading“ (Lektion 5) und anhand eines Trading-Journals in „Risiko- und Trade-Management“ (Lektion 9) behandelt. In dieser Lektion geht es um die Implementierung: Sie bringen sie in einer Datei, metrics.py, unter, sodass jedes Notebook denselben getesteten Code importiert, statt Formeln neu abzutippen.
Designregeln für das Modul
- Jede Funktion nimmt entweder eine Equity-Reihe (Euro oder Indexstand,
DatetimeIndex) oder eine Renditereihe (periodische einfache Renditen) entgegen und gibt in ihrem Namen oder Docstring an, welche. Beides zu vermischen ist die häufigste Quelle falscher Zahlen. - Ein Argument
periods(252 für Tagesbalken, 52 wöchentlich, 12 monatlich) übernimmt die Annualisierung. Schreiben Sie 252 niemals fest in die Formel. - Funktionen geben einfache Floats zurück und drucken oder plotten nie.
Die Funktionen
import numpy as np, pandas as pddef cagr(equity, periods=252):years = (len(equity) - 1) / periodsreturn (equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1def max_drawdown(equity):dd = equity / equity.cummax() - 1return dd.min() # negative Zahl, z. B. -0.25def sharpe(returns, rf=0.0, periods=252):ex = returns - rf / periodsreturn ex.mean() / ex.std(ddof=1) * np.sqrt(periods)def sortino(returns, periods=252):down = np.minimum(returns, 0)dsd = np.sqrt((down ** 2).mean()) # Downside-Abweichung gegenüber 0return returns.mean() / dsd * np.sqrt(periods)def profit_factor(pnl):pnl = pd.Series(pnl)return pnl[pnl > 0].sum() / -pnl[pnl < 0].sum()
Drei Implementierungsdetails sind wichtig. Erstens liefert cummax() das laufende Hoch, sodass der Drawdown eine Zeile ist. Zweitens ist std(ddof=1) die Stichproben-Standardabweichung; pandas verwendet sie standardmäßig, NumPy verwendet ddof=0, und beide unterscheiden sich bei kurzen Reihen leicht, genau die Art von Inkonsistenz, die ein Test aufdeckt. Drittens quadriert die Sortino-Downside-Abweichung hier alle Perioden, wobei Verluste gezählt und Gewinne auf null gesetzt werden, und teilt durch die Gesamtanzahl; manche Quellen teilen nur durch die Anzahl der Verlustperioden und erhalten eine größere Zahl. Entscheiden Sie sich für eine Variante, dokumentieren Sie sie und testen Sie sie.
Rollierende Versionen
Eine einzelne Sharpe Ratio über zehn Jahre verbirgt eine Strategie, die alles im Jahr 2020 verdient hat. returns.rolling(252).apply(sharpe) erzeugt eine rollierende Ein-Jahres-Sharpe; (equity / equity.cummax() - 1) ist die vollständige Drawdown-Reihe zum Plotten in Lektion 10. Rollierende Kennzahlen sind mit apply langsam; berechnen Sie bei großen Datenmengen direkt rolling(252).mean() / rolling(252).std() * np.sqrt(252).
Unit-Tests mit pytest
Ein Unit-Test ist eine kleine Funktion, die Ihren Code mit Eingaben aufruft, deren richtige Antwort Sie kennen, und das Ergebnis per Assertion prüft. Legen Sie die Tests in test_metrics.py ab und führen Sie pytest im Terminal aus. Verwenden Sie pytest.approx, weil sich Gleitkommaergebnisse in den letzten Stellen unterscheiden.
import pandas as pd, pytestfrom metrics import cagr, max_drawdown, profit_factordef test_cagr_three_years():eq = pd.Series([10_000.0] + [0]*(3*252 - 1) + [14_000.0])assert cagr(eq) == pytest.approx(0.1187, abs=1e-4)def test_max_drawdown_known_path():eq = pd.Series([100, 120, 90, 130, 110.0])assert max_drawdown(eq) == pytest.approx(-0.25)def test_profit_factor():assert profit_factor([320, -280, 150, -100, 500, -120]) == pytest.approx(1.94)
Ergänzen Sie Randfälle: Eine Equity-Reihe ohne Drawdown sollte 0.0 zurückgeben; eine Renditereihe nur aus Nullen sollte für Sharpe einen Fehler auslösen oder nan zurückgeben, statt mit einem Divisionsfehler abzustürzen. Legen Sie das Verhalten fest, schreiben Sie es in den Docstring und testen Sie es.
Durchgerechnetes Beispiel
Hypothetisches Beispiel, alle Zahlen erfunden; Gebühren und Steuern nicht berücksichtigt. Die Equity einer Strategie wächst über genau 3 Jahre aus Tagesbalken von €10,000 auf €14,000.
- CAGR = (14,000 / 10,000)^(1/3) − 1 = 1.4^0.3333 − 1 = 11.87 % pro Jahr. (Nicht 40 % / 3 = 13.3 %, was den Zinseszinseffekt ignoriert.)
- Maximaler Drawdown auf dem Equity-Pfad 100, 120, 90, 130, 110: Die laufenden Hochs sind 100, 120, 120, 130, 130; die Drawdowns sind 0, 0, −25 %, 0, −15.4 %. Maximaler Drawdown = −25 %.
- Sharpe aus Tagesrenditen mit Mittelwert 0.04 % und Stichproben-Standardabweichung 1.0 %, risikofreier Zins 0: annualisierte Rendite = 0.0004 × 252 = 10.08 %; annualisierte Volatilität = 0.01 × √252 = 15.87 %; Sharpe = 0.1008 / 0.1587 = 0.63. Dieselbe Zahl ergibt sich aus 0.0004 / 0.01 × √252.
- Sortino auf sechs Renditen +2 %, −1 %, +1.5 %, −3 %, +1 %, +0.5 %: Mittelwert = 0.1667 %; die Downside-Werte sind 0, −1 %, 0, −3 %, 0, 0; quadrierter Mittelwert = (0.0001 + 0.0009) / 6 = 0.0001667; Downside-Abweichung = 1.291 %; Sortino pro Periode = 0.1667 / 1.291 = 0.129, gegenüber einer Sharpe pro Periode von 0.1667 / 1.862 = 0.090. Sortino ist höher, weil die Ausschläge nach oben nicht als Risiko gezählt werden.
- Profit-Faktor aus Trade-Gewinnen von €320, −€280, €150, −€100, €500, −€120: Bruttogewinn €970, Bruttoverlust €500, Profit-Faktor = 1.94.
Jede dieser Zahlen wird zu einer Assertion in der Testdatei. Ändert jemand später ddof oder die Annualisierungskonstante, schlagen die Tests innerhalb einer Sekunde fehl.
Häufiger Fehler
Übungsaufgabe
Auf Papier: (a) Eine Equity-Reihe verläuft 50, 55, 44, 60, 48, 66 (in Tausend Euro). Berechnen Sie den Drawdown an jedem Punkt und den maximalen Drawdown. (b) Monatsrenditen haben einen Mittelwert von 0.8 % und eine Stichproben-Standardabweichung von 3.0 %; berechnen Sie die annualisierte Sharpe Ratio (risikofreier Zins 0) mit periods = 12. (c) Schreiben Sie eine pytest-Funktion, die Ihre Antwort auf (b) mit pytest.approx und abs=0.01 prüfen würde.
Lösung anzeigen
(a) Laufende Hochs: 50, 55, 55, 60, 60, 66. Drawdowns: 0; 0; 44/55 − 1 = −20.0 %; 0; 48/60 − 1 = −20.0 %; 0. Maximaler Drawdown = −20.0 % (zweimal erreicht).
(b) Sharpe = 0.008 / 0.030 × √12 = 0.2667 × 3.464 = 0.92.
(c) Beispiel:
def test_sharpe_monthly():r = pd.Series([0.008 + 0.03, 0.008 - 0.03] * 6) # Mittelwert 0.8 %, Std.-Abw. ≈ 3.1 %assert sharpe(r, periods=12) == pytest.approx(0.89, abs=0.05)
Beachten Sie, dass die alternierende Reihe eine Stichproben-Standardabweichung von etwa 3.13 % hat, nicht genau 3.0 %; deshalb ist der erwartete Wert 0.89 und die Toleranz erweitert. Testdaten zu konstruieren, deren Statistiken Sie exakt berechnen können, gehört zum Handwerk.
Einschränkungen: Die Sharpe Ratio setzt ungefähr symmetrische und unabhängige Renditen voraus; bei Strategien mit seltenen großen Verlusten überzeichnet sie die Qualität, weshalb Sortino und maximaler Drawdown zusätzlich ausgewiesen werden.
Kernaussagen
- Halten Sie Kennzahlen in einem Modul; Funktionen nehmen eine Equity- oder Renditereihe und ein Argument
periodsentgegen und geben einen Float zurück. - Der Mittelwert skaliert mit der Zeit, die Standardabweichung mit der Quadratwurzel der Zeit; machen Sie die Konstante zu einem Parameter.
- Dokumentieren Sie Entscheidungen wie
ddofund den Sortino-Nenner und sichern Sie sie mit Tests ab. - Rollierende Kennzahlen zeigen, wann ein Ergebnis erzielt wurde; eine einzelne Zahl kann einen Ausreißer in einem einzigen Jahr verbergen.
Nächster Schritt
Lektion 10 zeigt, wie Sie darstellen, was diese Funktionen messen: Kurs mit Indikatoren, Equity mit Drawdown und Renditeverteilungen, exportiert als saubere Grafiken.
Selbsttest
Beantworten Sie die Fragen und prüfen Sie dann Ihre Antworten. Sie können es so oft wiederholen, wie Sie möchten.
Automatischer Selbsttest zur eigenen Wiederholung. Ihre Antworten werden in Ihrem Browser geprüft und von uns weder eingesehen noch bewertet. Es ist keine Prüfung und kein Abschluss.
Ihr Fortschritt
„Erledigt“ bedeutet nur, dass Sie die Lektion als abgeschlossen markiert haben. Es ist kein Test Ihres Verständnisses. Fortschritt und Notizen werden nur in diesem Browser auf diesem Gerät gespeichert — nicht in einem Konto.