Die Geschichte nachspielen
Ein Backtest fragt, was eine Handelsregel mit vergangenen Daten verdient hätte. Die vektorisierte Version arbeitet mit ganzen Arrays: keine Schleife über die Tage, nur ein Signal pro Tag, eine Position pro Tag und eine Multiplikation mit den Renditen. Sie ist kurz, schnell und leicht zu prüfen, deshalb das erste Werkzeug, um Ideen zu sichten. Ihre Grenzen (keine Orders, kein Bargeld, keine Teilausführungen) sind genau das, was das nächste Thema hinzufügt.
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03]) # the asset's daily returns
signal = np.array([2.5, 0.4, -1.0, 0.0]) # any score: positive = buy, negative = sell
position = np.sign(signal) # +1, +1, -1, 0
print(position)Die Verzögerung um einen Tag
Ein aus dem heutigen Schlusskurs berechnetes Signal lässt sich erst nach diesem Schluss handeln, verdient also die Rendite von morgen, nicht die von heute. In Arrays ist das eine Verschiebung: strategy[t] = position[t - 1] * returns[t], ohne Position vor dem ersten Tag. Vergisst man sie, handelt die Strategie auf eine Rendite, die sie schon kennt, und der Backtest sieht wunderbar aus. Die Verschiebung ist eine Zeile, und die wichtigste der ganzen Funktion.
# Yesterday's position earns today's return
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03])
position = np.array([1.0, 1.0, -1.0, 0.0])
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
print(strategy) # [ 0. 0.02 -0.01 -0.03]
print(position * returns) # the same-day version: [ 0.01 0.02 0.01 0. ] looks better, and is cheatingEquity und Kennzahlen
Tägliche Renditen werden aufgezinst: Der Wert nach jedem Tag ist der Anfangswert mal das laufende Produkt der 1 + Rendite. Aus dieser Equity-Kurve fassen drei Zahlen einen Lauf zusammen. Die Gesamtrendite ist der Endwert minus 1. Die Sharpe-Ratio ist die mittlere Tagesrendite durch ihre Standardabweichung, mal sqrt(periods) zur Annualisierung. Der maximale Drawdown ist der schlimmste Rückgang von einem früheren Hoch, equity / running max - 1, der Verlust, den ein Anleger durchlebt hätte.
# Equity curve and the three statistics for a short run
import numpy as np
strategy = np.array([0.0, 0.02, -0.01, -0.03, 0.04])
equity = np.cumprod(1 + strategy)
print(np.round(equity, 5))
print(round(float(equity[-1] - 1), 5)) # total return
print(round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 3)) # annualised Sharpe
print(round(float((equity / np.maximum.accumulate(equity) - 1).min()), 5)) # maximum drawdown# A moving-average rule on a simulated price path, with its statistics
import numpy as np
rng = np.random.default_rng(7)
returns = rng.normal(0.0004, 0.01, size=500)
prices = 100 * np.cumprod(1 + returns)
fast = np.convolve(prices, np.ones(10) / 10, mode="valid")[-(len(prices) - 49):] # 10-day mean, aligned
slow = np.convolve(prices, np.ones(50) / 50, mode="valid") # 50-day mean
position = np.zeros(len(prices))
position[49:] = np.sign(fast - slow)
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
equity = np.cumprod(1 + strategy)
drawdown = equity / np.maximum.accumulate(equity) - 1
print(round(float(equity[-1] - 1), 4), round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 4), round(float(drawdown.min()), 4))- Position = Vorzeichen des Signals; die Strategierendite ist
position[t - 1] * returns[t]: Die heutige Rendite gehört zur gestrigen Entscheidung. - Die Equity ist
start * cumprod(1 + r); Gesamtrendite, annualisierte Sharpe-Ratio und maximaler Drawdown fassen sie zusammen. - Vektorisierte Backtests sind schnelle Filter: Sie ignorieren Kosten und Ausführung, und viele Versuche auf einer Historie erzeugen Glück.
Als Nächstes: ein ereignisgesteuerter Backtester, die Historie Bar für Bar mit Bargeld, Orders und Ausführungen nachspielen.
Lab: Was bewertet wird
- Mit
signal_to_positionein Signal in eine Position umsetzen25 Pkt. - Mit
strategy_returnsdie Position von gestern verdienen lassen25 Pkt. - Mit
equity_curvedie Tage aufzinsen25 Pkt. - Mit
backtest_statsden Lauf zusammenfassen25 Pkt.