Von Übungen zur Bibliothek
Sie haben hundert kleine Funktionen geschrieben. Eine Bibliothek sind diese Funktionen so angeordnet, dass jemand anderes (auch Sie in sechs Monaten) sie mit Vertrauen nutzen kann: eine klare öffentliche Schnittstelle, jedes Stück einzeln getestet, die Stücke zu einer Pipeline zusammengesetzt und das Ganze installierbar, dokumentiert und automatisch geprüft. Dieses Abschlussprojekt ist das Muster im Kleinen. Eine Strategiebibliothek ist der einfachste Ort, es zu üben: Das Signal macht aus Preisen Positionen, der Backtest macht aus Positionen Renditen nach Kosten, die Kennzahlen fassen die Renditen zusammen, und ein Runner verdrahtet sie. Jede Stufe nimmt einfache Arrays und liefert einfache Arrays, sodass sich jede einzeln mit Zahlen testen lässt, die man von Hand nachrechnen kann.
# The shape of a small library: a package, its tests, its metadata and its automated checks
tree = """\
quantlib/
├── pyproject.toml name, version, dependencies (numpy)
├── src/quantlib/
│ ├── __init__.py the public API: sma_positions, strategy_returns, performance_summary, run_strategy
│ ├── signals.py prices -> positions
│ ├── backtest.py positions -> returns after costs
│ └── metrics.py returns -> total return, volatility, Sharpe, drawdown
├── tests/ one test file per module, fixed seeds, hand-checked cases
└── .github/workflows/ci.yml lint, types, tests on every push
"""
print(tree.splitlines()[0]) # quantlib/
print(sum(1 for line in tree.splitlines() if ".py" in line)) # 4 the four module filesSignal und Backtest: ohne Look-ahead, mit Kosten
Das Signal ist hier eine Kreuzung gleitender Durchschnitte: long sein, wenn der schnelle Durchschnitt der Preise über dem langsamen liegt, short, wenn darunter, flat, wenn sie gleich sind oder der langsame noch nicht existiert. Die zwei Regeln, die entscheiden, ob ein Backtest ehrlich ist, sind im Code zu sehen. Erstens kein Look-ahead: Die am Schluss von Tag t entschiedene Position darf nur Preise bis Tag t nutzen und verdient die Rendite von Tag t + 1, weshalb die Rendite des Schritts t positions[t - 1] nutzt. Ein Test beweist es: Das Entfernen der letzten Preise darf frühere Positionen nicht ändern. Zweitens Kosten: Jede Positionsänderung zahlt cost * |Änderung|, sodass eine ständig wechselnde Strategie vor Kosten gewinnen und danach verlieren kann.
# The signal and the honest backtest: positions decided at the close are earned on the NEXT step, and trading costs money
import numpy as np
def sma_positions(prices, fast, slow):
prices = np.asarray(prices, dtype=float)
positions = np.zeros(len(prices), dtype=int)
for t in range(slow - 1, len(prices)):
f = prices[t - fast + 1 : t + 1].mean()
s = prices[t - slow + 1 : t + 1].mean()
positions[t] = 1 if f > s else -1 if f < s else 0
return positions
def strategy_returns(prices, positions, cost):
returns = []
for t in range(1, len(prices)):
before = positions[t - 2] if t >= 2 else 0
returns.append(positions[t - 1] * (prices[t] / prices[t - 1] - 1) - cost * abs(positions[t - 1] - before))
return np.array(returns)
prices = np.array([100, 101, 103, 102, 105, 108, 107, 110, 112, 111, 115, 118], dtype=float)
positions = sma_positions(prices, 2, 4)
print(positions) # [0 0 0 1 1 1 1 1 1 1 1 1] flat until the 4-day average exists, then long
print(np.allclose(sma_positions(prices[:8], 2, 4), positions[:8])) # True later prices never change earlier positions
print(np.round(strategy_returns(prices, positions, 0.0).sum() - strategy_returns(prices, positions, 0.001).sum(), 3)) # 0.001 one purchase at 0.1%Die Leistungskennzahlen und der Runner
Eine Renditereihe wird durch wenige Zahlen zusammengefasst. Die Gesamtrendite verzinst die Perioden, prod(1 + r) - 1; die Jahresrendite rechnet sie auf ein Jahr um, (1 + total)^(Perioden_pro_Jahr / n) - 1; die Jahresvolatilität ist die Stichproben-Standardabweichung mal sqrt(Perioden_pro_Jahr); die Sharpe-Ratio teilt die mittlere Rendite durch diese Abweichung, annualisiert, und ist die Hauptkennzahl für Rendite pro Risikoeinheit (0 für eine flache Reihe statt eines Divisionsfehlers); der maximale Drawdown ist der schlimmste Rückgang von einem Hoch der Kapitalkurve, die Zahl, die sagt, wie viel Schmerz die Strategie verlangt. Eine Runner-Funktion verbindet alles: Positionen, dann Renditen, dann die Zusammenfassung. Sie sollte nur die anderen Funktionen zusammensetzen, damit sich ein Fehler immer auf eine Stufe zurückführen lässt.
# Performance statistics of a return series, and the runner that wires the three stages together
import math
import numpy as np
def performance_summary(returns, periods_per_year):
returns = np.asarray(returns, dtype=float)
n = len(returns)
equity = np.concatenate(([1.0], np.cumprod(1 + returns)))
total = float(equity[-1] - 1)
deviation = float(np.std(returns, ddof=1))
peak = np.maximum.accumulate(equity)
return {
"total_return": total,
"annual_return": float((1 + total) ** (periods_per_year / n) - 1),
"annual_vol": deviation * math.sqrt(periods_per_year),
"sharpe": float(returns.mean() / deviation * math.sqrt(periods_per_year)) if deviation > 0 else 0.0,
"max_drawdown": float(np.max((peak - equity) / peak)),
}
summary = performance_summary([0.10, -0.20, 0.05], 252)
print(round(summary["total_return"], 4), round(summary["max_drawdown"], 4)) # -0.076 0.2 equity 1 -> 1.1 -> 0.88 -> 0.924: a 20% fall from the peak
print(performance_summary([0.0, 0.0, 0.0], 252)["sharpe"]) # 0.0 no variation: a Sharpe of 0, not an error# The runner only composes: if a number looks wrong you can test each stage on its own
def run_strategy(prices, fast, slow, cost, periods_per_year=252):
positions = sma_positions(prices, fast, slow)
returns = strategy_returns(prices, positions, cost)
return performance_summary(returns, periods_per_year)
# (sma_positions, strategy_returns and performance_summary are the functions you wrote in this lab)
steps = ["sma_positions", "strategy_returns", "performance_summary"]
print(" -> ".join(steps)) # sma_positions -> strategy_returns -> performance_summary
print(len(steps), "stages, one result: a dict of five numbers") # 3 stages, one result: a dict of five numbersIhr Abschlussprojekt
Bauen Sie nun Ihr eigenes, in Ihrem eigenen Repository. Wählen Sie eine von zwei Richtungen. Eine Bewertungsbibliothek: Black-Scholes und Greeks, ein Monte-Carlo-Bewerter mit antithetischen Variablen, ein Löser für die implizite Volatilität und die Kalibrierung eines Modells an einen Quote-Satz. Oder eine Strategiebibliothek: mehrere Signale, ein Backtest mit Kosten und Positionslimits, Walk-forward-Auswertung und ein Bericht. In beiden Fällen liefern Sie dasselbe: ein Paket mit klarer öffentlicher API und Typannotationen; Tests mit festen Seeds und von Hand geprüften Fällen; eine pyproject.toml und ein README, das Installation und ein Beispiel zeigt; einen CI-Workflow mit Lint, Typen und Tests; eine Performance-Notiz mit einem profilierten Engpass; und einen kurzen Text zu den Annahmen und dem, was das Modell brechen würde. Teilen Sie Ihre Zeit ein: erst die Schnittstelle entwerfen, die kleinste funktionierende Pipeline bauen, dann erweitern.
- Eine Bibliothek sind kleine getestete Funktionen, zu einer Pipeline hinter einer klaren Schnittstelle zusammengesetzt: Signal -> Positionen -> Renditen nach Kosten -> Kennzahlen, verdrahtet von einem Runner.
- Ein ehrlicher Backtest hat kein Look-ahead (die Position bei t nutzt Preise bis t und verdient die nächste Rendite) und berechnet Kosten bei jeder Positionsänderung.
- Fassen Sie mit Gesamt- und Jahresrendite, Volatilität, Sharpe und maximalem Drawdown zusammen; liefern Sie dann ein Paket mit Tests, CI, einem README und einem ehrlichen Bericht.
Damit ist der Lernpfad abgeschlossen. Sie haben nun die Sprache, die numerischen Werkzeuge, die Bausteine für Marktdaten und Ausführung, die Modelle und die Engineering-Gewohnheiten eines arbeitenden Quant-Entwicklers. Behalten Sie Ihr Abschluss-Repository: Es ist der beste Beleg für die hier aufgebauten Fähigkeiten.
Lab: Was bewertet wird
- Mit
sma_positionseinen Trend in Positionen verwandeln25 Pkt. - Mit Kosten in
strategy_returnsbacktesten25 Pkt. - Das Ergebnis mit
performance_summarybeurteilen25 Pkt. - Alles in
run_strategyverdrahten25 Pkt.