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

Der Beweis gegen Look-Ahead in Multi-Timeframe-Backtests: Die Zukunft stören, um zu beweisen, dass die Vergangenheit sie nicht sehen kann

Der Beweis gegen Look-Ahead in Multi-Timeframe-Backtests: Die Zukunft stören, um zu beweisen, dass die Vergangenheit sie nicht sehen kann
#Algotrading
#Backtest
#Look-Ahead-Bias
#Multi-Timeframe
#Datenleckage
#Validierung
🎯
Part 2 of 9 · Collection
Backtesting Without Fooling Yourself

Teil der Serie "Backtests ohne Illusionen".

Der vorherige Artikel endete mit einem Diagnosewerkzeug: dem One-Bar-Shift-Test. Verschiebe jede Order um eine Bar nach hinten; bricht die Performance ein, hast du in der Vergangenheit gehandelt. Dieser Test reicht für eine Einzel-Timeframe-Strategie aus, bei der sich Look-Ahead in der Ausführung versteckt — du hast auf Bar i entschieden und auf Bar i ausgeführt.

Multi-Timeframe-Strategien haben eine zweite, leisere Stelle, an der Leckage entstehen kann, und der Shift-Test erreicht sie nicht. Wenn eine Entscheidung auf einer 1-Minuten-Bar einen 1-Stunden-Trend konsultiert, liegt das Leck nicht mehr in der Order — es liegt im Indikator, berechnet aus einer Bar eines höheren Zeitrahmens, die sich noch gar nicht fertig gebildet hat. Dieser Artikel handelt von diesem Leck und davon, wie man — mechanisch, nicht durch bloßes Anstarren des Codes — beweist, dass die eigene Engine es nicht hat.

Die Behauptung, die wir am Ende aufstellen, ist stark: Unsere Multi-TF-Engine hat 25 von 25 Paritäts- und Leckage-Checks auf 86.400 Bars echter ETHUSDT-1m-Daten bestanden, einschließlich einer Shifted-Future-Probe, die jede zukünftige Bar stört und bestätigt, dass sich keine einzige vergangene Entscheidung bewegt. Und weil die Engine bitgenau identisch mit dem kanonischen Kern des Live-Bots ist, ist der Beweis eine Eigenschaft des Live-Systems, nicht nur des Backtests.

Das Minenfeld der Multi-Timeframes

Eine 1-Minuten-Entscheidung, die gleichzeitig in drei Zeitrahmen hineinreicht, wobei die Kerze des höchsten Zeitrahmens noch in Bildung ist und ihr endgültiger Schlusskurs hinter einer Zeitgrenze verborgen bleibt

Eine Multi-Timeframe-Strategie (Multi-TF) trifft eine Entscheidung auf einer schnellen Uhr, während sie langsame Uhren konsultiert. Unsere Engine ist eine konkrete Instanz davon: eine Triple-TF-Momentum-Regel, bei der ein Trend des höheren Zeitrahmens (HTF) und ein Trend des mittleren Zeitrahmens (MTF) Einstiege filtern (gaten) und ein Kreuz des niedrigeren Zeitrahmens (LTF) sie auslöst. Auf der 1m-Basisuhr feuert ein Einstieg nur, wenn ein 1m-HMA/HMA3-Kreuz mit der Richtung des 1h-Trends und des 15m-Trends übereinstimmt; ein Ausstieg feuert, wenn sich ein Trend eines höheren Zeitrahmens gegen die Position umkehrt oder der LTF zurückkreuzt.

Die Gefahr ist struktureller Natur. Jeder dieser Werte eines höheren Zeitrahmens muss auf jeder 1m-Bar beantwortet werden — 60-mal pro Stunde —, und jede Antwort darf nur das verwenden, was ein Live-Bot in diesem Moment wissen würde. In dem Moment, in dem auch nur eine dieser rund 180 Ablesungen pro Bar (drei Zeitrahmen, Ein- und Ausstiegstrends, Separations-Gates) eine Bar berührt, die noch nicht gedruckt wurde, liegt Look-Ahead vor. Die Angriffsfläche für einen Off-by-One-Fehler ist enorm, und anders als ein Ausführungsleck kündigt es sich nicht durch eine verschobene Order an.

Eine sich bildende Bar hat noch keinen endgültigen Schlusskurs

Hier ist die exakte Falle. Angenommen, es ist 10:37 Uhr und du entscheidest über die 1m-Bar, die gerade geschlossen hat. Deine Regel benötigt den 1h-Trend. Die 1h-Kerze, die den Zeitraum 10:00–11:00 abdeckt, bildet sich noch — ihr endgültiger Schlusskurs ist erst um 10:59:59 bekannt. Was weißt du um 10:37 Uhr tatsächlich? Nur den bisherigen laufenden Schlusskurs dieser Kerze, der dem Schlusskurs der 10:37-Bar entspricht. Der endgültige Schlusskurs der Kerze liegt 23 Minuten in der Zukunft.

Der naive Multi-TF-Backtest tut etwas, das völlig harmlos aussieht: Er resampelt die gesamte 1m-Reihe einmal im Voraus auf 1h und liest dann für jede 1m-Bar "den 1h-Schlusskurs" ab. Aber der Wert, den er für jede 1m-Bar zwischen 10:00 und 11:00 abliest, ist der endgültige Schlusskurs von 10:00–11:00 — eine Zahl, die in Echtzeit erst existiert, wenn die Stunde endet. Jede Entscheidung innerhalb der Stunde wird still und leise bis zu 59 Minuten in die Zukunft weitergereicht. Und das ist kein kleines Leck: Der Schlusskurs des höheren Zeitrahmens ist so ziemlich der stärkste mögliche Prädiktor für die kurzfristigen zukünftigen 1m-Renditen, die man gerade handeln will, sodass ihn durchsickern zu lassen fast dem Lesen des Lösungsschlüssels gleichkommt. Das ist der Indikator-Leckage-Kanal aus der Taxonomie, verstärkt: Der Blick nach vorn beträgt nicht eine Bar, sondern kann eine ganze HTF-Periode betragen.

Der One-Bar-Shift-Test aus dem Vorgängerartikel erfasst das nicht. Man kann die Order eine Bar später verschieben, und die resamplete 1h-Reihe bleibt trotzdem kontaminiert — das Leck lebt darin, wie der Indikator gebaut wurde, nicht darin, wann transaktioniert wurde.

Die Regel des Live-Bots, exakt reproduziert: Closed-Bar-Semantik

Der Puffer des höheren Zeitrahmens bei Bar i: eine Reihe abgeschlossener Kerzen-Schlusskurse, gefolgt von einer sich bildenden Kerze, deren Wert der aktuelle Basis-Schlusskurs ist, wobei der noch verborgene endgültige Schlusskurs durchgestrichen ist

Die korrekte Regel ist diejenige, die der Live-Bot bereits ausführt. In unserer Codebasis ist das eine kleine Klasse, RunningCandleBuffer, wortwörtlich aus dem Live-Tick-Simulator übernommen. Sie streamt Basis-Bars in Kerzen fester Periode und berechnet bei jeder Basis-Bar den HTF-Indikator aus einem sehr spezifischen Array:

all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)

Lies das wörtlich. self.closes sind die endgültigen Schlusskurse von Kerzen, die bereits geschlossen haben — eine Kerze wird nur angehängt, wenn eine neue Periodengrenze überschritten wird, und ihr gespeicherter Wert ist der Schlusskurs der letzten Basis-Bar innerhalb der Periode (candle_buffer.py:39–44). Die sich bildende Kerze trägt genau eine Zahl bei, self.current_close, den laufenden Schlusskurs — den Schlusskurs der jüngsten Basis-Bar, base_close[i]. Das ist eine Größe, die auf Bar i per Definition bekannt ist. Der endgültige Schlusskurs der sich bildenden Kerze wird nie verwendet, weil er noch nicht existiert.

Der HTF-Indikator um 10:37 sieht also [..., close(9:00 candle), close(10:37 so far)]. Wenn 10:38 gedruckt wird, aktualisiert sich der letzte Slot auf close(10:38). Wenn 11:00 überschritten wird, wird close(10:59) zum endgültigen Wert der neu geschlossenen Kerze, und ein frischer, sich bildender Slot öffnet sich. Zu keinem Zeitpunkt berührt eine Entscheidung innerhalb der Stunde den endgültigen Schlusskurs von 10:00–11:00. Das ist Closed-Bar-Semantik: geschlossene Kerzen tragen fertige Schlusskurse bei, die sich bildende Kerze trägt nur ihren laufenden Schlusskurs bei.

Unsere schnelle Engine (engine_multitf.py) ist eine vektorisierte, numba-kompilierte Neuimplementierung davon. Statt einer Python-Schleife mit wachsender Liste berechnet sie für jede Basis-Bar i im Voraus, wie viele Kerzen vollständig geschlossen haben (n_closed[i]), und legt das HMA/HMA3-Fenster als [closed candle closes…, base_close[i]] an — der laufende Schlusskurs wird im letzten Slot fixiert (engine_multitf.py:168–169). Es ist dieselbe Mathematik, für Geschwindigkeit über drei Zeitrahmen mit gerichteten Separations-Gates entrollt. Der Vertrag ist explizit: Der Wert bei Bar i hängt ausschließlich von base_close[0..i] ab.

Das ist die Behauptung. Der Rest des Artikels handelt davon, wie wir sie beweisen, denn eine Behauptung in einem Docstring ist wertlos.

Parität ist notwendig, aber nicht hinreichend

Eine vektorisierte numba-Engine, die eine Streaming-Klasse in explizite Schleifen entrollt, ist genau der Ort, an dem Off-by-One-Fehler entstehen. Deshalb ist das erste Tor ein bitgenauer Paritätstest gegen die kanonische Referenz, auf einem echten Datenausschnitt — ETHUSDT 1m, Januar–Februar 2024, 86.400 Bars.

Wir überprüfen zwei unabhängige Dinge gegen zwei unabhängige Referenzen:

  • Indikatoren und Kreuze gegen RunningCandleBuffer, Bar für Bar ausgeführt. Für jeden Zeitrahmen spielen wir die Live-Klasse über alle 86.400 Bars ab und vergleichen die Kreuzereignisse — die Bar, die Richtung, die Separation — auf exakte Übereinstimmung, plus die HMA/HMA3-Werte innerhalb einer Fließkomma-Toleranz (die Referenz verwendet np.dot, die Engine explizite Schleifen, sodass die Summationsreihenfolge bei ~1e-15 abweicht). Die Kreuze stimmen exakt überein: 408 Kreuze beim HTF (1h, HMA-Länge 21), 2.792 beim MTF (15m/14), 3.691 beim LTF (1m/50). Kein einziges Kreuzereignis unterscheidet sich in Bar oder Richtung.
  • Trades gegen eine unabhängige Simulation der Handelsregeln in reinem Python, angetrieben von den eigenen Kreuzen der Referenz. Dies reproduziert die Schleife des Live-Backtests — Einstiegstrends filtern Einstiege, eine Umkehr des Ausstiegstrends oder ein gegenläufiges LTF-Kreuz schließt, Ausführung auf open[i+1], 0,09 % Round-Trip-Gebühr, Zwangsschließung auf der letzten Bar — ganz ohne die numba-Maschinerie der Engine. Anschließend vergleichen wir Trade für Trade: Ein-/Ausstiegs-Bars, Seiten, Ein-/Ausstiegspreise, PnL, Ausstiegsgründe und die gesamte aktive Zeit in der Position.

Die Separationsschwellen im Test sind keine harmlosen Standardwerte. Sie sind so gewählt, dass sie eine unbequeme Ecke treffen — eine MTF-Ausstiegs-Schwelle, die höher gesetzt ist als die zugehörige Einstiegs-Schwelle —, was den Zweig "erste Definition eines Ausstiegstrends bei offener Position" erzwingt, den die Referenz als Umkehr behandelt. Parität muss auch in den Grenzfällen gelten, nicht nur auf dem einfachen Pfad.

Feld für Feld sind die Trades identisch: 466 Trades für die duale Konfiguration, 211 Trades für die triple, mit übereinstimmendem Gesamt-PnL bis auf 1e-12 und allen Trade-Feldern innerhalb der Toleranz gleich. Zwei Implementierungen, die keinen Code teilen — eine kompilierte, vektorisierte Engine und eine naive Python-Schleife auf den Kreuzen einer dritten Implementierung — produzieren die gleichen Trades bis zur letzten Dezimalstelle.

Das ist ein starkes Ergebnis, und es ist kein Beweis gegen Look-Ahead. Parität sagt, dass die schnelle Engine die Referenz treu reproduziert. Wenn die Referenz selbst geleckt hätte — wenn RunningCandleBuffer einen Blick vorausgeworfen hätte —, würde die Parität dieses Leck treu reproduzieren und bestehen. Übereinstimmung zwischen Implementierungen sagt dir, dass sie dasselbe sind, nicht dass sie kausal sind. Für Kausalität braucht man eine andere Art von Test, einen, der die Engine direkt fragt, ob die Vergangenheit die Zukunft sehen kann.

Die Shifted-Future-Probe: der eigentliche Beweis

Ein vertikaler Schnitt bei 60 Prozent der Reihe: alles rechts davon multipliziert und durchgeschüttelt, alles links davon bitgenau eingefroren und identisch

Die Definition von Look-Ahead ist operational, also teste sie operational. Look-Ahead bedeutet, dass eine vergangene Entscheidung von zukünftigen Daten abhängt. Die Kontraposition ist ein Test, den man ausführen kann: Wenn man die Zukunft verändert und sich irgendeine vergangene Entscheidung bewegt, hat die Vergangenheit die Zukunft gelesen. Also verändere die Zukunft — brutal — und beobachte die Vergangenheit.

Wähle einen Schnittpunkt j bei 60 % der Reihe (Bar 51.840 von 86.400). Störe jede Bar ab j: multipliziere alle zukünftigen Schlusskurse und Eröffnungskurse mit 1,05. Berechne den gesamten Signalstapel für alle drei Zeitrahmen auf den gestörten Daten neu. Weise dann nach, dass alles streng vor j bitgenau identisch mit dem ungestörten Lauf ist:

j = int(n * 0.6)                       # bar 51,840
cl2 = cl.copy(); cl2[j:] *= 1.05       # shove the future up 5%
op2 = op.copy(); op2[j:] *= 1.05

base = [precompute_tf_signals(cl,  ts, p, L) for (p, L) in tf_params]
pert = [precompute_tf_signals(cl2, ts, p, L) for (p, L) in tf_params]

for s0, s1 in zip(base, pert):
    assert eq_nan(s0.hma[:j],  s1.hma[:j])      # HMA identical, NaNs included
    assert eq_nan(s0.hma3[:j], s1.hma3[:j])
    assert np.array_equal(s0.cross[:j], s1.cross[:j])   # every cross event
    assert np.array_equal(s0.sep[:j],   s1.sep[:j])     # every separation

Nicht "annähernd". Nicht "innerhalb einer Toleranz". np.array_equal, wobei NaNs mit NaNs übereinstimmen müssen — jeder HMA-Wert, jeder HMA3-Wert, jedes Kreuz-Flag und jede Separation über die 51.840 vergangenen Bars muss dieselbe Fließkommazahl sein. Dann dieselbe Behauptung für Trades: Jeder Trade, dessen Ausstieg vor j liegt, muss Feld für Feld unverändert sein. Wenn ein 5-prozentiges Anschieben der Zukunft einen einzigen vergangenen HMA-Wert in der zwölften Dezimalstelle bewegt, hat eine vergangene Entscheidung die Zukunft konsultiert, und die Probe schlägt fehl.

Unsere Engine besteht sie — für alle drei Zeitrahmen und sowohl für die duale als auch die triple Trade-Simulation. Das Stören von 34.560 zukünftigen Bars lässt 51.840 vergangene Bars und jeden Trade, der zwischen ihnen geschlossen wurde, exakt so, wie sie waren. Das ist keine Übereinstimmung zwischen Implementierungen; es ist ein direkter Nachweis, dass die Informationsgrenze in der Zeit hält.

Ein Test, der nicht fehlschlagen kann, beweist nichts

Es gibt eine Möglichkeit, die obige Probe zu bestehen, die überhaupt nichts beweist: Wäre die Störung ein No-op — würde die Multiplikation der Zukunft mit 1,05 nirgendwo irgendetwas verändern —, dann wäre "die Vergangenheit ist unverändert" trivial wahr und völlig uninformativ. Ein grüner Haken bei einem Test, der nicht fehlschlagen kann, ist schlimmer als kein Test, weil er falsches Vertrauen erzeugt. Deshalb trägt die Probe zwei weitere Behauptungen, die ihr Biss verleihen.

Die Zukunft muss sich tatsächlich verändern. Wir weisen nach, dass die Störung irgendwo in [j, n) tatsächlich Kreuze verändert hat:

assert not np.array_equal(s0.cross[j:], s1.cross[j:])   # probe has teeth

Jetzt ist das Ergebnis aussagekräftig: Dasselbe 5-prozentige Anschieben, das die Zukunft umgeschrieben hat, hat die Vergangenheit bitgenau identisch gelassen. Die Störung ist real, sie pflanzt sich nach vorn fort und stoppt exakt am Schnittpunkt. Ein einseitiges Leck — Vergangenheit liest Zukunft — hätte rückwärts über j hinweg geblutet; das tut es nicht.

Die Grenze liegt genau bei der aktuellen Bar — nicht eine zu früh, nicht eine zu spät. Ein subtileres Versagen wäre eine Engine, die kausal, aber veraltet ist: Sie ignoriert den laufenden Schlusskurs der aktuellen Bar und reagiert eine Bar zu spät (kein Leck, aber ein Verzug, den es im Live-Handel nicht gäbe), oder eine, die eine Bar zu früh reagiert (ein Ein-Bar-Leck). Also stören wir eine einzelne Bar j (mit dem Faktor 1,02) und weisen zwei Dinge gleichzeitig nach: Die Vergangenheit [0, j) bleibt unberührt, und hma[j] reagiert sofort.

cl3 = cl.copy(); cl3[j] *= 1.02        # nudge exactly one bar
s3 = precompute_tf_signals(cl3, ts, p_ltf, L_ltf)
assert eq_nan(s0.hma[:j], s3.hma[:j])  # nothing before j moves
assert s0.hma[j] != s3.hma[j]          # bar j reacts on the same bar

Das legt die Grenze präzise fest. Der laufende Schlusskurs der sich bildenden Kerze fließt bei Bar j mit null Verzögerung und null Vorwissen in den Indikator ein: Bar j sieht ihren eigenen Schlusskurs sofort, und keine frühere Bar sieht ihn überhaupt. Das ist genau die Rasierklingen-Kante, auf der die Closed-Bar-Semantik sitzen soll, und der Test bestätigt, dass die Engine dort sitzt.

Hier ist das vollständige Tor — alle 25 Checks, die zwischen dieser Engine und einem gefälschten Backtest stehen:

Gruppe Was jeder Check nachweist Anzahl
Indikatoren & Kreuze (×3 Zeitrahmen) Kreuzereignisse exakt gegen RunningCandleBuffer; Separation bei Kreuzen; HMA/HMA3-Werte (rtol 1e-9) 9
Trades (dual + triple) Trade-Anzahl; Feld für Feld identisch; Gesamt-PnL bis 1e-12; aktive Zeit in Position 8
Shifted-Future-Probe (dual + triple) vergangene Signale bitgenau unverändert; Probe hat Biss (Zukunft hat sich tatsächlich verändert); Trades vor j unverändert; Einzel-Bar-Störung ist lokalisiert 8
Gesamt 25

Die ersten beiden Gruppen belegen, dass die schnelle Engine die Live-Referenz ist. Die dritte belegt, dass die Referenz kausal ist. Man braucht alle drei: eine schnelle Engine, die leckt, eine leckende Engine, die kausal ist, und eine kausale Engine, die hinterherhinkt, sind drei verschiedene Fehlerarten, und das Tor schließt jede davon aus.

Warum die Probe zeitrahmenagnostisch ist

Die Eleganz der Shifted-Future-Probe besteht darin, dass sie nicht weiß oder sich darum kümmert, wo ein Leck sitzen würde. Sie erwähnt niemals Zeitrahmen, Resampling oder Kerzengrenzen. Sie fragt nur: Bewegt das Stören der Zukunft die Vergangenheit? Das macht sie genau zum richtigen Werkzeug für das Multi-TF-Leck, das ein Shift-the-Fill-Test verfehlt.

Betrachte den naiven Bug des Resamplens der gesamten Reihe im Voraus direkt. Wäre der 1h-Stream durch Resampling der gesamten Reihe im Voraus aufgebaut worden, wäre der bei Bar j-1000 (gut innerhalb der Stunde, die nach j schließt) abgelesene "1h-Schlusskurs" der endgültige Schlusskurs einer Kerze, deren endgültiger Schlusskurs von Bars bei und jenseits von j abhängt. Multipliziert man die Zukunft mit 1,05, ändert sich dieser endgültige Schlusskurs — also ändert sich der HTF-Indikator bei j-1000, das Gate bei j-1000 ändert sich, und eine vergangene Entscheidung bewegt sich. Die Probe würde sofort im HTF-Stream aufleuchten, bei einer Bar, die tausend Schritte vor dem Schnittpunkt liegt.

Der HTF-Stream unserer Engine bewegt sich nicht, weil die sich bildende Kerze bei j-1000 nur base_close[j-1000] beiträgt — einen vergangenen Schlusskurs — und der endgültige Schlusskurs der Kerze schlicht nie konsultiert wird, bis die Grenze überschritten ist. Die Probe ist blind gegenüber dem Mechanismus und fängt den Bug trotzdem ein, was genau das ist, was man von einem Beweis will: Er schränkt das Verhalten ein (keine vergangene Entscheidung hängt von zukünftigen Daten ab), statt die Implementierung zu prüfen (haben wir das Resampling korrekt indiziert?). Verhalten ist das, was gehandelt wird; Implementierung ist das, von dem man hofft, dass es damit übereinstimmt.

Backtest und Live teilen sich eine Wahrheit

Zwei Panels, die sich eine einzige, mit RunningCandleBuffer beschriftete Wirbelsäule teilen: ein Backtest-Sweep auf der einen Seite, ein Live-Bot auf der anderen, beide an denselben Kern angeschlossen

Es gibt noch einen weiteren Grund, warum das wichtiger ist als eine typische Backtest-Prüfung. Die Referenz, gegen die die Engine bewiesen wird — RunningCandleBuffer — ist keine Test-Fixture, die geschrieben wurde, um den Backtest gut aussehen zu lassen. Es ist die eigene Kerzenlogik des Live-Bots, wortwörtlich aus dem Tick-Simulator entnommen, der in der Produktion läuft. Die Closed-Bar-Regel, die die Probe validiert, ist die Regel, die der Live-Bot Bar für Bar ausführt.

Das Paritätstor leistet also doppelte Arbeit. Es beweist, dass die schnelle Engine mit der Referenz übereinstimmt, und weil die Referenz der Live-Kern ist, beweist es, dass die schnelle Engine mit Live übereinstimmt. Der Vorgängerartikel warnte, dass ein Leck die sauberste Erklärung für eine Backtest-Live-Paritätslücke ist — der Live-Bot ist der einzige Ort, an dem man mechanisch nicht vorausschauen kann, sodass ein Backtest, der vorausschaut, in dem Moment auseinanderdriftet, in dem er live geht. Hier ist diese Lücke konstruktionsbedingt geschlossen: Backtest und Bot teilen sich einen Kerzenpuffer, eine Kreuzregel, eine Definition von "bekannt bei Bar i". Die Zahl, die die Suche optimiert, ist die Zahl, die der Bot tatsächlich handeln kann.

Das ist der ganze Sinn davon, die Abwesenheit von Look-Ahead zu beweisen, statt sie anzunehmen. Eine Multi-TF-Suche durchläuft Tausende von Konfigurationen; würde die Engine lecken, würde die Suche die Konfiguration finden, die das Leck am aggressivsten ausnutzt, und dir einen gefälschten Gewinner in die Hand drücken — den Fehlermodus, den die Taxonomie mit einer Sharpe Ratio von 15 aus reinem Rauschen gemessen hat. Die Probe ist das, was einem erlaubt, darauf zu vertrauen, dass der Gewinner echt ist, bevor man ihn mit Kapital verbindet.

Was die Probe beweist und was nicht

Strenge gegenüber dem Test schneidet in beide Richtungen, deshalb muss man über ihren Umfang präzise sein. Die Shifted-Future-Probe beweist eine einzige, spezifische Eigenschaft: In diesen Daten hängt keine Signal- oder Trade-Entscheidung bei oder vor Bar j von irgendeiner Bar nach j ab — die Informationsgrenze in der Zeit hält durch den Indikator-, Kreuz- und Trade-Pfad hindurch. Das ist genau das Multi-TF-Leck, das wir beseitigen wollten, und es ist die Eigenschaft, die ein Code-Review nicht feststellen kann.

Sie beweist nicht, dass die Strategie einen Edge hat. Eine perfekt kausale Engine kann ehrlich Geld verlieren, und die Probe schweigt dazu — wie es sein sollte; die Abwesenheit von Leckage zu beweisen und einen echten Edge zu finden, sind getrennte Aufgaben, und sie zu verwechseln ist die Art, wie geleckte Backtests eingesetzt werden. Sie deckt nicht die nicht-zeitlichen Verzerrungen ab: Survivorship-Bias beim Instrument, Selektionsverzerrung dadurch, dass die Probe erst ausgeführt wird, nachdem die Engine gut ausgesehen hat, oder ein zu großzügiges Gebührenmodell. Und sie beweist für sich genommen nicht, dass Live-Ausführungen mit Backtest-Ausführungen übereinstimmen — Slippage und Latenz sind reale Lücken, die die Probe nicht sehen kann, weil sie auf dem Entscheidungspfad operiert, nicht am Ausführungsort. Was diese Lücke schließt, ist die separate Tatsache, dass der Kerzenkern der Engine bitgenau identisch mit dem des Live-Bots ist.

Ein ehrlicher Vorbehalt zum Design der Probe selbst: Sie schneidet an einem einzigen j (60 % der Reihe). Die Eigenschaft, die sie überprüft, ist uniform in j — an Bar 51.840 ist nichts Besonderes —, sodass ein Schnitt ein fairer Test einer strukturellen Eigenschaft ist, eine paranoidere Version aber j über die gesamte Reihe hinweg durchfahren würde. Wir halten einen gut gewählten Schnitt plus die Einzel-Bar-Lokalisierungsprüfung für ausreichend, weil ein Leck, das sich vor einer 34.560-Bar-Zukunftsstörung versteckt, aber bei einem anderen Schnitt auftaucht, ein sehr seltsamer Bug sein müsste. Der Punkt ist, die Grenzen der eigenen Evidenz zu kennen, nicht so zu tun, als sei ein einzelner Test ein universeller Quantor.

Kernpunkte

  1. Multi-Timeframe-Strategien lecken durch die sich bildende Bar, nicht durch die Order. Eine Entscheidung innerhalb einer unfertigen HTF-Periode darf nur den laufenden Schlusskurs dieser Kerze verwenden (den jüngsten Basis-Schlusskurs), niemals ihren endgültigen Schlusskurs. Backtests, die die gesamte Reihe im Voraus resampeln, reichen jede Entscheidung innerhalb der Periode bis zu einer vollen HTF-Periode in die Zukunft weiter.
  2. Der One-Bar-Shift-Test erreicht dieses Leck nicht. Er erfasst Ausführungs-Look-Ahead; das Multi-TF-Leck lebt darin, wie der Indikator gebaut wurde. Man braucht eine andere Probe.
  3. Die Live-Regel exakt reproduzieren, dann beweisen. Wir haben die Closed-Bar-Semantik des RunningCandleBuffer des Bots als vektorisierte Engine nachgebaut und sie hinter 25 Checks gestellt: Kreuze exakt gegen die Referenz (408 / 2.792 / 3.691), Trades Feld für Feld identisch (466 dual, 211 triple), PnL bis 1e-12.
  4. Parität ist notwendig, aber nicht hinreichend. Mit einer Referenz übereinzustimmen beweist, dass man dasselbe ist, nicht dass man kausal ist. Eine geleckte Referenz, treu reproduziert, leckt immer noch.
  5. Die Shifted-Future-Probe ist der eigentliche Beweis. Störe jede Bar bei oder nach j; weise nach, dass jedes Signal und jeder Trade vor j bitgenau unverändert bleibt. Wenn die Zukunft die Vergangenheit bewegen kann, liegt Look-Ahead vor.
  6. Gib der Probe Biss. Weise nach, dass sich die Zukunft tatsächlich verändert hat (die Störung ist kein No-op) und dass ein Einzel-Bar-Stupser auf ebendieser Bar reagiert (kein Leck, kein Verzug). Ein Test, der nicht fehlschlagen kann, beweist nichts.
  7. Wenn Backtest und Live sich einen Kern teilen, überträgt sich der Beweis. Weil die Engine bitgenau identisch mit der Kerzenlogik des Live-Bots ist, ist die Abwesenheit von Look-Ahead auch eine Eigenschaft des Live-Systems — und die Backtest-Live-Paritätslücke schließt sich konstruktionsbedingt.

Der Vorgängerartikel zeigte, wie ein einzeiliges Leck eine Sharpe Ratio von 15 fabriziert. Dieser hier zeigt die entgegengesetzte Disziplin: nicht "wie Lecks einen täuschen", sondern wie man mechanisch und auf echten Daten beweist, dass eine bestimmte Engine nicht leckt. Störe die Zukunft. Zuckt die Vergangenheit nicht zusammen, handelst du in der Gegenwart.

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.