← Zurück zu den Artikeln
July 2, 2026
5 min read

Die Framework-Steuer: Wenn deine Backtest-Bibliothek langsamer ist als eine naive Pandas-Schleife

Die Framework-Steuer: Wenn deine Backtest-Bibliothek langsamer ist als eine naive Pandas-Schleife
#Algo-Trading
#Backtest
#Performance
#Parametersuche
#Vektorisierung
#Benchmark
Part 2 of 10 · Collection
High-Performance Backtest Engines

Teil der Serie „Backtests ohne Illusionen".

Am Grund der meisten Algo-Trading-Projekte liegt eine bequeme Annahme: dass eine ausgereifte, gut bestternte Backtest-Bibliothek schnell ist. Sie hat jahrelange Beiträge, eine echte Ereignisschleife, ein Broker-Modell, ein Kommissionsschema. Sicher schlägt sie die zusammengehackte Pandas-Schleife, die du selbst geschrieben hättest. Also greifst du danach, verdrahtest deine Strategie und startest einen Parameter-Sweep — ein paar tausend Konfigurationen, ein Über-Nacht-Job. Du kommst am Morgen zurück, und er läuft immer noch.

Wir haben acht Backtest-Engines auf einem einzigen identischen Parameter-Sweep gemessen und etwas gefunden, das ändern sollte, wie du ein Werkzeug für die Suche auswählst. Zwei der beliebtesten quelloffenen ereignisgesteuerten Frameworks — backtrader und bt — liefen den Sweep langsamer als eine naive Pandas-Schleife, die wir als Wegwerf-Baseline geschrieben hatten. Nicht ein wenig langsamer. backtrader brauchte etwa 2,5× so lange wie die Pandas-Baseline; bt etwa 4,7×. Unterdessen lief eine vektorisierte/kompilierte Engine auf derselben Arbeit rund 13.000× schneller als bt.

Das ist die Framework-Steuer. Beliebte Backtester sind für einen ehrlichen Lauf gebaut — eine Strategie, ein Datensatz, sorgfältige Fills, ein Broker, der sich wie ein Broker verhält. Genau das willst du für eine finale Validierung oder einen Live-Paritäts-Check. Es ist genau das falsche Werkzeug für das, womit Algo-Research tatsächlich seine Zeit verbringt: dieselbe Strategie zehntausendmal mit leicht anderen Stellschrauben laufen zu lassen. Dieser Artikel misst die Steuer, erklärt den Mechanismus und gibt eine Entscheidungsregel dafür, wann ein „echtes" Backtest-Framework die falsche Wahl ist.

Jede Zahl hier stammt aus einem einzigen reproduzierbaren Prüfstand (benchmarks/bench_oss_engines.py, Commit 250dbb5) auf einer identischen, paritätsfixierten Arbeitslast. Wo wir eine Engine nicht selbst laufen ließen, sagen wir das und stellen es in einen separaten Ehrlichkeitsabschnitt, statt eine Zahl zu erfinden.

Ein Lauf gegen zehntausend

Dieselbe Strategieberechnung fließt entlang dreier auseinanderlaufender Pfade: eine ereignisgesteuerte Spur in Rot-Orange, gezwungen, an jeder Bar-Station anzuhalten, ein Gewichte-/Rebalancing-Baum aus Algo-Knoten in Bernstein und ein einziger vektorisierter Smaragd-Cyan-Strahl, der die ganze Serie auf einmal auflöst — das Paradigma ist das Schicksal der Sweep-Geschwindigkeit

Die entscheidende Tatsache der Parametersuche ist, dass die Engine tausendfach läuft, aber die Analyse nur einmal geschieht. Welche Fixkosten du auch immer für das Aufsetzen eines Backtests zahlst — die Ereignisschleife bauen, einen Broker instanziieren, ein Objekt pro Bar allozieren —, du zahlst sie bei jeder einzelnen Kombination. Kosten, die in einem Lauf unsichtbar sind („wen kümmern 6 Sekunden?"), werden über einen Sweep zur gesamten Rechnung („6 Sekunden × 10.000 = 16,6 Stunden").

Backtest-Engines fallen in drei Paradigmen, und das Paradigma ist das Schicksal der Sweep-Leistung:

  1. Ereignisgesteuert — die Engine läuft Bar für Bar, emittiert Ereignisse, ruft deinen next()/onBars()-Callback auf, leitet Orders durch ein Broker-Objekt. Das ist die Architektur von backtrader, backtesting.py, PyAlgoTrade, zipline und nautilus_trader. Sie spiegelt wider, wie Live-Trading tatsächlich funktioniert, was genau der Grund ist, warum ihr für Realismus vertraut wird — und genau der Grund, warum sie langsam ist: der Python-Overhead pro Bar wird 150.000-mal pro Kombination gezahlt.
  2. Gewichte/Rebalancingbt sitzt hier. Du übergibst ihm eine Zielgewichtsmatrix, und es rebalanciert an den Daten, die du angibst. Kein Callback pro Bar, aber immer noch ein Objektgraph pro Ereignis (ein Baum aus Algos, ein Transaktionsbuch), der in Python ausgewertet wird.
  3. Vektorisiert / kompiliert — die ganze Strategie wird als Array-Operationen (vectorbt), ein JIT-kompilierter Kernel (numba) oder nativer Code (Rust, ein MLX-GPU-Kernel) ausgedrückt. Es gibt überhaupt kein Python pro Bar. Die Schleife, falls es eine gibt, läuft mit Maschinengeschwindigkeit.

Der Rest dieses Artikels ist die empirische Konsequenz jener Taxonomie. Wir haben eine Arbeitslast gebaut, jede Engine beweisbar dieselbe Arbeit tun lassen und die Zeit gemessen.

Die Arbeitslast: eine Strategie, achtzig Stellschrauben, für alle identisch

Ein Benchmark ist nur ehrlich, wenn jede Engine denselben Job macht. Unserer ist bewusst schlicht — eine Arbeitslast, bei der das Einzige, was sich zwischen den Engines unterscheidet, die Engine selbst ist.

  • Daten. Eine einzige synthetische Close-Reihe aus geometrischer Brownscher Bewegung: 150,000 Bars, seed=42, Volatilität pro Bar sigma=0.0008, x0=30000. Deterministisch, sodass jeder sie Bit für Bit reproduzieren kann. Nur Close per Konstruktion — OHLC wird auf jeder Etappe auf den Close gesetzt, weil die Strategie ein Close-Cross ist.
  • Strategie. Ein Hull-Moving-Average-Cross: ein HMA der Länge L gegen eine schnellere HMA-eines-Drittels-Variante (HMA gegen HMA3). Immer im Markt, bei jedem Cross Wechsel zwischen Long/Short. Das ist ein echter, nicht-trivialer Indikator — zwei verschachtelte gewichtete gleitende Durchschnitte plus ein Wurzel-Fenster-Glätter — keine Spielzeug-SMA, sodass die Arbeit pro Bar repräsentativ ist.
  • Der Sweep. 80 HMA-Längen, die 6..200 überspannen. Das sind die „zehntausend Läufe", klein genug gemacht, um sie direkt zu messen: 80 unabhängige Kombinationen, jede ein vollständiger Backtest über 150k Bars.
  • Kosten. Eine Roundtrip-Gebühr von 0.09%, pro Seite aufgeteilt für Engines, die Seiten-Kommissionen modellieren. Fill auf derselben Bar bei close[i] — das Signal auf Bar i wird zum Close dieser Bar ausgeführt, die Konvention, die unsere Produktions-Engine verwendet.

Der Timer pro Kombination umschließt genau zwei Dinge: die numpy-HMA-Vorberechnung und den Engine-Lauf. Aufsetzen, das wirklich einmalig ist (Daten laden, die Bar-Objekte bauen), sitzt außerhalb des Timers. Es gibt einen Aufwärmlauf, dann Best-of-N-Wiederholungen, und — weil die ereignisgesteuerten Engines langsam genug sind, dass 80 volle Kombinationen viele Minuten bis über eine Stunde brauchen würden — messen wir die Zeit einer gleichmäßigen Stichprobe des Gitters und extrapolieren linear. Kombinationen sind unabhängig, also ist lineare Extrapolation im Erwartungswert exakt; dieselbe Konvention wird auf die Pandas-Baseline angewandt, sodass keine Engine dadurch bevorteilt wird.

Parität: beweisen, dass alle dieselbe Arbeit tun

Hier ist die Falle, in die ein naiver Engine-Vergleich tappt: eine „schnelle" Engine könnte einfach weniger tun. Wenn backtrader 2.700 Trades verbucht und deine vektorisierte Engine 40, dann ist die vektorisierte Engine nicht schneller — sie ist falsch, und der Vergleich ist bedeutungslos.

Also sperren wir den Vergleich mit einem Trade-Anzahl-Paritäts-Check fest. Bei L=104 produziert die numpy-Referenz genau 2.707 geschlossene Trades. Jede Engine muss das innerhalb einer Toleranz von ±1 reproduzieren, sonst bricht der Lauf mit einer work-parity FAILED-Assertion ab. Die Toleranz existiert nur, weil Engines über Buchführungskonventionen uneins sind — ob die letzte offene Position zwangsgeschlossen und gezählt wird, ob der anfängliche Einstieg ein „Trade" ist — nicht über die Trades selbst:

Engine Gemeldete Trades bei L=104 Konvention
numpy-Referenz 2707 geschlossene Roundtrips
backtesting.py 2708 +1: letzte Position am Ende zwangsgeschlossen
backtrader 2707 letzte offene Position nicht gezählt
bt 2708 +1: anfänglicher Einstieg als Transaktion gezählt
PyAlgoTrade 2708 +1: anfänglicher Einstieg als Fill gezählt

Jede Engine landet bei 2707 ± 1. Welche Geschwindigkeitsunterschiede sich auch herausstellen, sie sind kein Artefakt einer Engine, die stillschweigend Arbeit auslässt. Das ist die Disziplin, die es uns erlaubt, ein ereignisgesteuertes Framework und einen GPU-Kernel in dieselbe Tabelle zu stellen und es ernst zu meinen.

Die Ergebnisse

Hier ist die ganze Tabelle, sortiert von schnellsten zu langsamsten. combos/s ist der Durchsatz; die letzte Spalte ist, wie lange der volle 80-Kombinationen-Sweep dauert. Die Baseline-Zeile ist M0 — die naive Pandas-Engine, eine for-Schleife über Bars mit skalarer Buchführung, das Ding, das du an einem Nachmittag schreiben und wegwerfen würdest. Alles, was langsamer als diese Baseline ist, steht fett.

Engine combos/s Paradigma Voller 80-Kombinationen-Sweep
MLX-GPU-Kernel 779 vektorisiert (Apple GPU) 0.10 s
natives Rust ~350 kompiliert 0.23 s
mp + numba 246 kompilierter JIT + Multiprozess 0.33 s
vectorbt 56.9 vektorisiert (numpy/numba) 1.4 s
numba (Single-Core) 39.7 kompilierter JIT 2.0 s
backtesting.py 1.42 ereignisgesteuert 56 s
PyAlgoTrade 0.51 ereignisgesteuert 2.6 min
M0 — naives pandas + Schleife 0.28 skalare Baseline 4.8 min
backtrader 0.11 ereignisgesteuert 12.7 min
bt 0.06 Gewichte / Rebalancing 22.5 min

Lies die Tabelle von oben nach unten, und das Paradigma sortiert sich selbst: die oberen fünf sind alle vektorisiert oder kompiliert, die unteren fünf alle ereignisgesteuert oder objektgraphbasiert — mit der naiven Pandas-Schleife, die über zwei ausgereiften, beliebten Frameworks sitzt. Die Spanne von oben nach unten beträgt vier Größenordnungen. Auf genau derselben 2.707-Trade-Arbeitslast beendet der MLX-Kernel den Sweep in einem Zehntel einer Sekunde; bt braucht zweiundzwanzigeinhalb Minuten. Das ist ein Faktor von rund 13.000×.

Der Skandal in der Mitte der Tabelle

Das Paradox in der Mitte der Tabelle: ein schlichter grau-blauer naiver for-Schleifen-Läufer, der vor zwei verzierten, funktionsschweren ereignisgesteuerten Framework-Maschinen die Ziellinie überquert, die rot-orange glühen und noch hinterher schuften, beschwert von Broker-Modellen und Analyzer-Stapeln — die bescheidene Baseline schlägt die ausgereiften Frameworks auf der identischen 2.707-Trade-Arbeitslast

Die ins Auge springenden Zahlen sind an den Extremen, aber das lehrreiche Ergebnis ist in der Mitte: backtrader (0,11 combos/s) und bt (0,06 combos/s) sind beide langsamer als die naive Pandas-Baseline (0,28 combos/s).

Das verdient es, zu setzen. M0 ist keine clevere Engine. Es ist eine Python-for-Schleife, die in einen DataFrame indexiert, eine Position und Cash in einfachen Skalaren verfolgt und Trades an eine Liste anhängt — die bewusst unoptimierte „Kontrolle", die wir aufgenommen haben, um etwas offensichtlich Schlechtes zum Schlagen zu haben. Pandas' Zugriff pro Zeile ist notorisch langsam, und wir haben uns darauf gestützt. Und dennoch verlieren zwei der am meisten empfohlenen Backtest-Bibliotheken im Ökosystem gegen sie: backtrader um 2,5×, bt um 4,7×.

Die Nuance, die das ehrlich hält: nicht jede ereignisgesteuerte Engine ist langsamer als pandas. backtesting.py (1,42 combos/s) schlägt die Baseline um 5×, weil es eine schlanke, numpy-gestützte Ereignisschleife ist, die die Objekterzeugung pro Bar minimal hält. PyAlgoTrade (0,51) zieht ebenfalls an der Baseline vorbei. Also ist „ereignisgesteuert" kein automatisches Todesurteil — aber je schwerer die Maschinerie pro Bar, desto schlimmer wird es, und backtrader und bt tragen hier die schwerste Maschinerie. Das Paradigma setzt die Obergrenze; die Implementierung entscheidet, wo unter dieser Obergrenze du landest.

Der Punkt ist nicht, dass dies schlechte Bibliotheken sind. backtraders Broker-Modell und bts Baum-aus-Algos-Design existieren, um dir Korrektheit und Ausdruckskraft zu erkaufen — realistische Order-Behandlung, Portfolio-Rebalancing, Analyzer. Diese Features haben Laufzeitkosten, und diese Kosten sind unsichtbar, wenn du einmal läufst. Über einen Sweep sind sie die ganze Geschichte.

Warum ereignisgesteuerte Engines die Steuer zahlen

Wo die Framework-Steuer erhoben wird, pro Bar: eine einzelne Bar-Iteration in ihre kostspieligen Komponenten zerlegt — ein materialisiertes Bar-Objekt, ein Callback-Frame, ein Broker, der eine Order leitet, ein Buch, an das angehängt wird — jedes eine rot-orange glühende Mautstation, die eine Münze abschöpft, wiederholt entlang eines weiten, zurückweichenden Korridors aus zwölf Millionen identischen Bar-Iterationen

Der Mechanismus ist nicht mysteriös. Ein ereignisgesteuerter Backtest tut pro Bar etwa dies:

  1. Rückt die Uhr vor, schneidet die nächste Bar aus dem Datenfeed und materialisiert sie als Objekt (ein Bar, eine Line, ein dict).
  2. Feuert einen Callback in den Nutzercode (next(), onBars()), was ein Python-Funktionsaufruf mit seinem eigenen Frame ist.
  3. Innerhalb des Callbacks fragt er den Broker-/Positionszustand ab, wieder über Methodenaufrufe und Attributsuchen.
  4. Falls eine Order erstellt wird, leitet er sie durch den Broker: validiert sie, prüft Margin/Cash, plant einen Fill, mutiert ein Portfolio-Objekt, hängt an ein Transaktionsbuch an.
  5. Aktualisiert Analyzer, Observer und jede Buchführung, die das Framework unterhält.

Multipliziere nun mit 150.000 Bars, dann mit 80 Kombinationen: zwölf Millionen Bar-Iterationen pro Sweep, jede eine Handvoll Objekt-Allokationen auf Python-Ebene und dynamischer Dispatches. Pythons Overhead pro Operation — Dutzende bis Hunderte Nanosekunden für eine Attributsuche oder eine kleine Allokation — ist einmal trivial und zwölf Millionen Mal ruinös. Der bt-Fall ist eine Variante derselben Krankheit: obwohl es nur an Trade-Daten statt an jeder Bar rebalanciert, wertet jedes Rebalancing einen Baum aus Algo-Objekten aus und berührt ein pandas-gestütztes Portfolio-Buch, und davon gibt es 2.707 pro Kombination, mal 80.

Die naive Pandas-Schleife schlägt backtrader und bt aus einem stumpfen Grund: sie tut weniger pro Bar. Sie überspringt den Broker, die Ereignisobjekte, den Analyzer-Stapel, die Order-Routing-Zustandsmaschine. Sie zahlt pandas' hässliche Steuer pro Zeile, aber diese eine hässliche Steuer ist immer noch billiger als die ordentliche, funktionsvollständige Ein-Objekt-pro-Ereignis-Steuer des Frameworks. Wenn du einen Backtest auf „Position × nächste Rendite − Gebühren" herunterbrichst, ist das meiste von dem, was ein Framework pro Bar tut, Overhead, den du während einer Suche nicht nutzt.

Und das ist die Falle: der Overhead ist der Grund, warum du das Framework gewählt hast. Du wolltest den realistischen Broker. Du wolltest die Analyzer. Während der finalen Validierung willst du all das. Während einer 10.000-Kombinationen-Suche, aus der du am anderen Ende nur ein einziges skalares Ziel abliest, zahlst du für eine Limousine, um Runden zu drehen.

Das andere Ende der Tabelle: vektorisiert und kompiliert

Der obere Teil der Tabelle ist, was passiert, wenn du das Python pro Bar vollständig löschst.

  • vectorbt (56,9 combos/s) drückt die ganze Strategie als numpy/numba-Array-Operationen aus. Es gibt keine Bar-Schleife in Python — das Signal, die Position, der PnL sind alle auf Array-Ebene. Es läuft den Sweep in 1,4 Sekunden gegenüber bts 22,5 Minuten: etwa 950× schneller auf identischer Arbeit. (Wir behandeln vectorbts Design tiefer in der vectorbt-Übersicht und dem breiteren pandas-vs-polars-Vergleich.)
  • numba (39,7) ist die Pandas-Schleife, unverändert in der Form, JIT-kompiliert zu nativem Code. Derselbe Algorithmus wie M0, aber @njit verwandelt 0,28 combos/s in 39,7 — eine ~140× Beschleunigung durch einen Dekorator, weil der Interpreter-Overhead, der die skalare Schleife dominierte, einfach verdampft.
  • mp + numba (246) lässt den kompilierten Kernel über CPU-Kerne laufen. Kombinationen sind peinlich parallel — jede ist unabhängig — sodass Multiprocessing nahezu linear über dem JIT skaliert.
  • natives Rust (~350) entfernt den letzten Rest des Python-Klebstoffs: der ganze Sweep ist nativ.
  • MLX-GPU (779) bildet den Sweep auf einen Apple-Silicon-GPU-Kernel ab. 80 Kombinationen werden zu 80 parallelen Rechenspuren; der Sweep ist fertig, bevor du die Enter-Taste loslässt.

Zwei Dinge sind es wert, präzise benannt zu werden. Erstens: numba beweist, dass das Paradigma mehr zählt als die Sprache. M0 und numba laufen den gleichen Algorithmus — der Unterschied ist rein, dass das eine interpretiertes Python pro Bar und das andere kompiliert ist. Das ist die gesamte Framework-Steuer in einem kontrollierten A/B: ~140× für das Löschen des Interpreters aus der inneren Schleife. Zweitens: der Sprung von numba (39,7) zu mp+numba (246) zu MLX (779) geht überhaupt nicht mehr um die Engine — es geht um Orchestrierung und Hardware. Sobald die Steuer pro Bar weg ist, wird Geschwindigkeit zu einer Frage, wie viele Kombinationen du parallel und auf welchem Silizium laufen lässt. Wir gehen die volle Progression in der Backtest-Engine-Geschwindigkeitsleiter durch, und den Grund, warum die letzte Meile von Prozess-/Serialisierungskosten dominiert wird, im IPC-Steuer-Artikel.

Was wir nicht gemessen haben (und warum wir es dir sagen)

Die Glaubwürdigkeit eines Benchmarks lebt in dem, was er sich weigert zu fälschen. Wir haben acht Engines end-to-end unter Parität laufen lassen. Mehrere bekannte Frameworks haben wir keine Zahl gegeben, und wir listen sie lieber ehrlich auf, als eine Zahl zu extrapolieren, die wir nicht gemessen haben:

  • zipline / zipline-reloaded — ereignisgesteuert, Quantopian-Abstammung. Schweres Aufsetzen (ein voller Handelskalender und ein Daten-Bundle), was ein Äpfel-mit-Äpfeln-Timing pro Kombination heikel macht. Architektonisch sitzt es mit backtrader im ereignisgesteuerten Lager; wir würden es nahe jenem Ende der Tabelle erwarten, aber wir haben es nicht bewiesen.
  • nautilus_trader — ereignisgesteuert mit einem Rust/Cython-Kern, explizit für Live-Parität entworfen. Sein Kern ist kompiliert, also ist es die ereignisgesteuerte Engine, die am ehesten die volle Python-Steuer nicht zahlt — eine wirklich interessante Messung, die wir noch nicht durchgeführt haben.
  • QuantConnect Lean — C#-basiert, eine ganz andere Laufzeit; nicht direkt vergleichbar in einem Python-Prüfstand.
  • Jesse — ereignisgesteuert, krypto-fokussiert; wir haben sein Design in einer separaten Notiz besprochen, es aber hier nicht gebencht.
  • QSTrader — ereignisgesteuert, portfolio-orientiert; dieselben Paradigma-Vorbehalte.
  • fastquant — wir haben es versucht; die Installation/API war in unserer Umgebung kaputt, also gibt es keine Zahl. Wir werden keine raten.

Zwei ehrliche Vorbehalte zu den Zahlen, die wir tatsächlich berichten. Die vectorbt-, numba-, mp+numba-, natives-Rust- und MLX-Zahlen stammen aus unserer eigenen Engine-Leiter auf der identischen Arbeitslast, nicht aus dem OSS-Prüfstand, der die vier ereignisgesteuerten Zeilen produzierte — es ist dieselbe Arbeitslast, aber ein anderer Messaufbau, und die natives-Rust-Zahl ist ein ungefähres ~350, keine straffe Messung. Und absolute combos/s sind hardwarespezifisch; was übertragbar ist, ist die Reihenfolge und die Verhältnisse, die groß genug sind (13.000× von oben nach unten, 2,5–4,7× für die pandas-gegen-Framework-Umkehrung), dass kein vernünftiger Hardware-Unterschied sie umkippt.

Zur Verteidigung ereignisgesteuerter Engines

Die ereignisgesteuerte Engine in ihrer eigentlichen Rolle: ein einzelner akribischer, hochtreuer Validierungslauf einer einzigen überlebenden Strategie, gerendert als präzises Smaragd-und-Cyan-Instrument, das einen realistischen Fill modelliert, Margin und den Order-Lebenszyklus respektiert — dieselbe Maschinerie pro Bar, die während der Suche reine Verschwendung war, hier als wertvolles Handwerk gezeigt, eine sorgfältige Generalprobe vor dem Deployment

Es wäre leicht, das als „ereignisgesteuerte Backtester sind schlecht" zu lesen. Das ist die falsche Lektion und eine unfaire.

Ereignisgesteuerte Engines sind für einen anderen Job gebaut, und sie sind gut darin. Der Broker pro Bar, der Order-Lebenszyklus, die Fill-Logik, die Analyzer — diese Maschinerie existiert, um einen Backtest dem Live-Trading so nahe wie möglich ähneln zu lassen. Wenn dein Ziel eine einzige, vertrauenswürdige Generalprobe einer Strategie ist, die du gleich deployen wirst, willst du, dass die Engine über jeden Fill schwitzt, Teilausführungen modelliert, Margin respektiert und sich weigert, dich zu Preisen handeln zu lassen, die du nicht hättest bekommen können. Diese Treue ist das Produkt. Ihre Laufzeitkosten sind der Preis des Realismus, und für einen Lauf ist der Preis vernachlässigbar.

Das Versagen ist nicht die Engine; es ist, sie für die falsche Phase zu verwenden. Research hat zwei distinkte Phasen mit entgegengesetzten Anforderungen:

  • Suche will Durchsatz. Du erkundest eine Landschaft, von der das meiste Müll ist, und du musst tausende Punkte bewerten, um die wenigen zu finden, die einen zweiten Blick wert sind. Treue pro Punkt zählt kaum — du rankst, du deployest nicht. Hier ist die Framework-Steuer reine Verschwendung.
  • Validierung will Treue. Du hast eine Handvoll Kandidaten und musst so präzise wie möglich wissen, ob sie realistische Ausführung, Gebühren, Slippage und die Look-Ahead-Fallen überleben, die Papierrenditen aufblähen. Hier verdient die ereignisgesteuerte Engine ihre Kosten.

Der Fehler, den die Framework-Steuer bestraft, ist, deine Suche auf deiner Validierungs-Engine laufen zu lassen — Limousinen-Tarife zu zahlen, um eine Landschaft zu erkunden, von der du 99 % wegwerfen wirst.

Die Entscheidungsregel

Die praktische Erkenntnis komprimiert sich zu einer einzigen Entscheidung:

Suche auf einer vektorisierten/kompilierten Engine. Validiere die Überlebenden auf einer ereignisgesteuerten.

Konkret:

  1. Baue oder leihe einen schnellen Kernel für den Sweep. vectorbt, wenn du ihn von der Stange willst; eine numba-kompilierte Schleife, wenn deine Strategie sich nicht sauber vektorisieren lässt (der @njit-Dekorator allein hat hier ~140× erkauft). Lass den vollen Parameterraum hindurchlaufen.
  2. Lass niemals einen großen Sweep auf backtrader, bt, zipline oder irgendeinem schweren ereignisgesteuerten Framework laufen. Wenn ein Sweep auf einem davon dein Engpass ist, ist die Lösung keine größere Maschine — es ist die falsche Engine. Selbst die naive Pandas-Schleife würde zwei davon schlagen.
  3. Befördere eine Shortlist zur ereignisgesteuerten Engine für einen Treue-Check. Nimm die Handvoll Überlebender und lass sie auf der realistischen Engine erneut laufen, wo das Broker-Modell und die Fill-Logik Probleme aufdecken können, die der schnelle Kernel wegabstrahiert hat.
  4. Erzwinge Parität zwischen den beiden. Die schnelle Engine und die Treue-Engine müssen sich bei Trade-Anzahlen und PnL auf einer festen Konfiguration einig sein (unser ±1-Trade-Check bei L=104), sonst messen Suche und Validierung verschiedene Strategien und die ganze Pipeline ist eine Lüge.

Das ist dieselbe Zwei-Geschwindigkeiten-Architektur, die immer auftaucht, wenn Suche und Validierung entgegengesetzte Kostenprofile haben, und es ist der Grund, warum unser eigener Stack einen schnellen vektorisierten/kompilierten Pfad für die Parametersuche behält und die schwere Maschinerie für die finale Zielfunktions-Auswertung und Plateau-Checks reserviert.

Erkenntnisse

  1. Beliebt ≠ schnell für Sweeps. Auf einer einzigen identischen, paritätsfixierten Arbeitslast (150k Bars, 80 HMA-Cross-Kombinationen, 2.707 Trades) liefen backtrader (0,11 combos/s) und bt (0,06) beide langsamer als eine naive Pandas-Schleife (0,28). Ein ausgereiftes, gut bestterntes Framework ist nicht automatisch die schnelle Wahl.
  2. Die Framework-Steuer ist pro Bar, und ein Sweep multipliziert sie. Zwölf Millionen Bar-Iterationen pro Sweep, jede trägt ein Ereignisobjekt, einen Callback und einen Broker-Roundtrip. Kosten, die in einem Lauf unsichtbar sind, sind die ganze Rechnung über zehntausend.
  3. Das Paradigma setzt die Obergrenze. Vektorisierte/kompilierte Engines (vectorbt 56,9, numba 39,7, mp+numba 246, natives Rust ~350, MLX 779) schlagen die ereignisgesteuerten Engines um zwei bis vier Größenordnungen — bis zu ~13.000× von oben nach unten. Derselbe Algorithmus, bloß JIT-kompiliert, lief 140× schneller.
  4. Nicht alle ereignisgesteuerten Engines sind gleich. backtesting.py (1,42) und PyAlgoTrade (0,51) schlagen immer noch die naive Baseline; die Steuer skaliert mit der Schwere der Maschinerie pro Bar. Die Implementierung entscheidet, wo unter der Obergrenze du landest.
  5. Zwei Engines, zwei Phasen. Suche auf einem vektorisierten/kompilierten Kernel; validiere die Überlebenden auf der realistischen ereignisgesteuerten Engine. Erzwinge Trade-Anzahl-/PnL-Parität zwischen ihnen, damit beide dieselbe Strategie messen.
  6. Sei ehrlich darüber, was du gemessen hast. Wir haben acht Engines unter Parität gebencht und die aufgelistet, die wir nicht gemessen haben (zipline, nautilus_trader, Lean, Jesse, QSTrader und die fastquant-Installation, die nicht laufen wollte), statt Zahlen für sie zu erfinden.

Die unbequeme Zusammenfassung: wenn ein Parameter-Sweep dein Engpass ist, liegt das Problem wahrscheinlich nicht an deiner Maschine und nicht an deiner Strategie. Es liegt daran, dass du eine Suche auf einer Engine laufen lässt, die für einen einzigen ehrlichen Lauf gebaut wurde — und du wärst schneller mit der Pandas-Schleife, die dir zu peinlich war, um sie zu behalten.

Der volle Prüfstand, die Paritäts-Assertions und die rohen JSON-Ergebnisse pro Engine leben in benchmarks/bench_oss_engines.py und benchmarks/results_oss/ beim Commit 250dbb5. Für das kompilierte/GPU-Ende der Leiter siehe die Backtest-Engine-Geschwindigkeitsleiter und die IPC-Steuer-Analyse.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.