Eine Bar nach der anderen
Ein ereignisgesteuerter Backtester verhält sich wie ein Live-System: Die Zeit schreitet Bar für Bar voran, und bei jeder Bar sieht das Programm nur die Vergangenheit, entscheidet, sendet eine Order und aktualisiert sein Konto. Er ist langsamer als ein vektorisierter, trägt aber Zustand (Bargeld, Aktien), kann Orders und Ausführungen modellieren, und derselbe Strategiecode kann später auf einem Live-Feed laufen. Die Schleife ist der Kern: ein for über die Bars, das Konto wandert von Durchlauf zu Durchlauf.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0 # not enough history yet
fast_mean = sum(history[-fast:]) / fast
slow_mean = sum(history[-slow:]) / slow
return 1 if fast_mean > slow_mean else 0
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
print(crossover_signal(prices[:3], 2, 4)) # 0: only 3 prices
print(crossover_signal(prices[:6], 2, 4)) # 1: the 2-bar mean is above the 4-bar mean
print(crossover_signal(prices[:9], 2, 4)) # 0: the trend has turnedOrders verändern das Konto
Eine Order macht aus Bargeld Aktien oder umgekehrt. Bei ganzen Aktien lässt der Kauf von int(cash // price) etwas Bargeld übrig, und der Kontowert ist immer cash + shares * price. Die Orderlogik in einer kleinen Funktion execute_order zu halten, verkürzt die Schleife und erlaubt es, Gebühren und Slippage später an einer Stelle zu ergänzen. Alles zu verkaufen ist einfach cash + shares * price und null Aktien.
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price) # whole shares only
return cash - bought * price, shares + bought
return cash + shares * price, 0
cash, shares = execute_order(1000.0, 0, 103.0, 1)
print(cash, shares, cash + shares * 103.0) # 73.0 9 1000.0
cash, shares = execute_order(cash, shares, 99.0, 0)
print(cash, shares) # 964.0 0Die Schleife und was sie wissen darf
Bei Bar t darf die Strategie prices[:t] nutzen, das beim vorherigen Schluss endet, und handelt zu prices[t]. Das Slicing prices[:t] hält die Zukunft draußen: Der nächste Kurs steht schlicht nicht in der Liste. Daraus folgt ein guter Test für eine Ereignis-Engine: Läuft sie nur über die ersten k Bars, müssen die Ergebnisse den ersten k Ergebnissen des vollen Laufs entsprechen. Weichen sie ab, hat die Strategie gespäht.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0
return 1 if sum(history[-fast:]) / fast > sum(history[-slow:]) / slow else 0
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price)
return cash - bought * price, shares + bought
return cash + shares * price, 0
def run_backtest(prices, fast, slow, cash):
shares = 0
values = []
for t, price in enumerate(prices):
if t > 0:
target = crossover_signal(prices[:t], fast, slow) # only what was known at the last close
cash, shares = execute_order(cash, shares, price, target)
values.append(cash + shares * price)
return values
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
full = run_backtest(prices, 2, 4, 1000.0)
print([round(v, 2) for v in full])
print(run_backtest(prices[:6], 2, 4, 1000.0) == full[:6]) # True: the future never leaks# The benchmark: buy once at the start and hold
import random
def buy_and_hold(prices, cash):
shares = int(cash // prices[0])
left = cash - shares * prices[0]
return [left + shares * price for price in prices]
random.seed(3)
prices = [100.0]
for _ in range(99):
prices.append(round(prices[-1] * (1 + random.gauss(0.0004, 0.012)), 2))
held = buy_and_hold(prices, 10000.0)
print(round(held[0], 2), round(held[-1], 2))- Ein ereignisgesteuerter Backtester läuft mit Zustand (Bargeld, Aktien) über die Bars; bei Bar
tdarf er nurprices[:t]nutzen. - Orders bewegen zwischen Bargeld und ganzen Aktien: Der Kontowert ist
cash + shares * price; halten Sie die Orderlogik in einer Funktion. - Prüfen Sie auf Look-ahead: Ein Lauf über die ersten
kBars muss den erstenkErgebnissen des vollen Laufs gleichen; vergleichen Sie mit Buy-and-Hold.
Als Nächstes: Look-ahead-Bias, der stille Fehler, der einen Backtest zu gut macht, um wahr zu sein.
Lab: Was bewertet wird
- Mit
crossover_signalzwei Durchschnitte vergleichen25 Pkt. - Mit
execute_orderdie Order ausführen25 Pkt. - Mit
run_backtestBar für Bar durchlaufen25 Pkt. - Mit
buy_and_holddie Vergleichsgröße setzen25 Pkt.