Hier ist eine Kerze. Binance USDⓈ-M Perpetual, BTCUSDT, die Minute beginnend um 13:46:00 UTC. Sie kam vom Klines-Endpunkt als reines Array mit zwölf Werten, und sie ist die mit Abstand häufigste Eingabeeinheit im Retail-nahen Quant-Research. Fast jede Strategie, die wir generieren, fasst irgendetwas an, das so aussieht.
Also nehmen wir sie auseinander, Index für Index, und schauen, wie viel davon ein Backtest falsch versteht.
| Index | Wert | Name |
|---|---|---|
| 0 | 1773495960000 | Open-Zeit (ms) |
| 1 | "84120.50" | Open |
| 2 | "84177.90" | High |
| 3 | "84098.10" | Low |
| 4 | "84163.40" | Close |
| 5 | "38.417" | Base-Volumen (BTC) |
| 6 | 1773496019999 | Close-Zeit (ms) |
| 7 | "3232108.94" | Quote-Volumen (USDT) |
| 8 | 1204 | Anzahl der Trades |
| 9 | "21.883" | Taker-Buy Base-Volumen |
| 10 | "1841203.55" | Taker-Buy Quote-Volumen |
| 11 | "0" | ignore |
[0] und [6]: welche Minute ist das, und nach wessen Uhr
Open-Zeit 1773495960000, Close-Zeit 1773496019999. Man beachte die zweite Zahl: Sie endet auf 19999, eine Millisekunde vor dem Open der nächsten Kerze. Das Zeitfenster ist halboffen, und die Börse sagt das ausdrücklich. Die Hälfte der Datenbugs, die ich je verfolgt habe, begannen damit, dass jemand beide Endpunkte als inklusiv behandelt und einen Trade an der Grenze doppelt gezählt hat, oder ein Resampling so ausgerichtet hat, dass jede 5-Minuten-Kerze eine Millisekunde von ihrer Nachbarin geborgt hat.
Die Zeitstempel stammen von der Matching-Engine der Börse. Nicht von deiner Uhr, nicht von der Ingestion-Uhr deines Datenanbieters, und auch nicht von derselben Uhr, die der Funding-Snapshot verwendet. Wenn du eine Kline-Serie mit einer Funding-Rate-Serie oder einer Open-Interest-Serie aus einem anderen Endpunkt zusammenführst, führst du Quellen zusammen, die im Schnitt nur auf etwa eine Sekunde genau übereinstimmen. Für eine 1-Minuten-Strategie ist das ein Timing-Fehler von 1,7 % bei jeder verknüpften Zeile. Für alles unterhalb einer Minute ist das fatal.
Und diese Kerze ist auf ihren Open gestempelt. Die darin enthaltene Information ist erst um 13:46:59.999 überhaupt bekannt. Jede Frage zur Indexierungskonvention – verschiebe ich um eins, verwende ich Bar-Close- oder Bar-Open-Zeitstempel als Event-Zeit – ist im Grunde dieselbe Lookahead-Frage im Kostüm.
[1] "84120.50": das Open, das man nicht handeln kann
Das Open ist der Preis des ersten Trades, der innerhalb des Zeitfensters gedruckt wurde. Es ist eine abgeschlossene Transaktion zwischen zwei anderen Marktteilnehmern. Bis diese Kerze als Zeile in deinem Dataframe existiert, ist dieser Preis schon sechzig Sekunden alt.
Die vorherige Kerze schloss bei 84109.80, es liegen also 10,70 $ Preisbewegung in der Lücke zwischen zwei benachbarten 1-Minuten-Kerzen. Das sind 1,3 Basispunkte, unsichtbar in einem Chart, und etwa ein Viertel der Taker-Fee. Passt. Aber schau dir dieselbe Serie auf einem Alt-Perp während einer US-CPI-Veröffentlichung an, und diese Inter-Bar-Lücken laufen auf 15 bis 40 Basispunkte. Eine Backtest-Engine, die auf Bar-Close signalisiert und im nächsten Bar-Open füllt, nimmt still an, dass die Lücke null ist – und sie ist genau dann null, wenn es nicht darauf ankommt.
[2] und [3]: "84177.90" High, "84098.10" Low
An diesen beiden gehen die meisten Fill-Engines zugrunde, deshalb ist dieser Abschnitt der lange.
High und Low sind extreme berührte Preise. Sie tragen keine Größe und keine Dauer. Aus dem rohen Trade-Tape für dieselbe Minute waren alles bei oder unter 84100.00 gerade 0,62 BTC über neun Prints innerhalb eines 1,4-Sekunden-Fensters. Ein Backtest-Stop bei 84100 "füllt" also – und wenn deine Position 3 BTC groß ist, hast du auf dem Weg nach unten das ganze sichtbare Orderbuch gegessen, und der Rest deiner Größe wurde irgendwo im Rückschwung 84105–84130 ausgeführt. Die Kerze sagt, das Low war 84098.10. Die Kerze sagt nicht, dass dort nur 52.000 $ Notional gehandelt wurden.
Die andere Richtung ist schlimmer, weil sie dich beschönigt. Angenommen, du hattest eine ruhende Limit-Sell-Order bei 84175. Das High der Kerze ist 84177.90, also füllt eine naive Engine dich bei 84175 und verbucht Maker-Rebate statt Taker-Kosten. Ob dieser Fill tatsächlich stattgefunden hat, hängt von der Warteschlangenposition auf diesem Preislevel ab, was die Kerze nicht wissen kann und du wahrscheinlich nie erfasst hast. Berührt ist nicht gefüllt.
Die Regel, auf die wir uns in unserer Fill-Engine geeinigt haben: Eine ruhende Limit-Order füllt nur, wenn die Kerze das Level durchhandelt, nicht nur bis dorthin reicht. Fills am exakten Extremtick erfordern einen Volume-at-Price-Nachweis aus dem Trade-Tape, sonst werden sie abgelehnt. Das hat etwa 6 % der Trades aus einem typischen Mean-Reversion-Buch entfernt und den gebacktesteten Sharpe eines Kandidaten von 1,9 auf 1,1 gesenkt. Dieser Kandidat war nie real; die Fill-Regel war nur das Erste, das ehrlich genug war, das zu sagen.
Verwandte Falle: der Intrabar-Pfad. Wenn die Range einer Kerze sowohl deinen Stop als auch dein Take-Profit umfasst, kann OHLCV dir nicht sagen, was zuerst kam. Jede Engine muss eine Konvention wählen. Unsere nimmt immer Stop-first an, was pessimistisch ist, gelegentlich falsch, aber niemals einen falschen Gewinn erzeugt. Wenn deine Engine Take-Profit-first annimmt, erzeugen breite Kerzen Gewinn aus purer Mehrdeutigkeit – und breite Kerzen sind genau die, die deine PnL-Verteilung dominieren.
[4] "84163.40": die am wenigsten robuste Zahl in der Kerze
Der Close ist der letzte Print im Zeitfenster. Das ist alles. Es könnte ein 0,002-BTC-Odd-Lot eines Bots sein, der um 13:46:59.8 eine Position abrundet. Dieser einzelne, strukturell willkürliche Tick ist das, worauf die meisten Research-Pipelines jedes Signal berechnen, jede Position markieren und jeden Exit bewerten.
Bei BTC-Perps spielt das kaum eine Rolle; bei einem dünnen Altcoin-Perp um 04:00 UTC spielt es enorm eine Rolle, und der Unterschied zwischen den Closes zweier Venues für dieselbe Minute kann deinen gesamten Edge pro Trade übersteigen. Wenn die PnL einer Strategie spezifisch vom Close abhängt, lassen wir sie stattdessen mit dem Mark-Preis der Börse neu laufen, der indexbasiert und deutlich schwerer zu manipulieren ist. Wenn die Ergebnisse divergieren, hat die Strategie das Artefakt gehandelt.
[5] und [7]: "38.417" und "3232108.94", Volumen in welchen Einheiten
Base-Volumen ist BTC; Quote-Volumen ist USDT. Beides hier vorhanden, was nicht jede Venue als Höflichkeit bietet. Der Grund, warum das wichtig ist, ist die Aggregation über Venues hinweg. Coin-margined Contracts werden in Kontrakten zu 100 $ Notional quotiert. Manche Aktien-Feeds melden Round Lots. Prediction-Market-Venues melden Share-Zahlen, wobei ein Share ein in Dollar denominierter binärer Claim ist. "Volumen" über ein gemischtes Universum zu summieren, ohne auf eine einheitliche Notional-Einheit zu normalisieren, ergibt ein Liquiditätsranking, das reiner Unsinn ist – und zwar Unsinn, der auf stabil wirkende Art die Prüfung überlebt.
Alles beim Ingest auf Quote-Notional normalisieren. Das Rohfeld auch speichern, aber niemals eine Strategie darauf zugreifen lassen.
[8] 1204: das Feld, das dein Impact-Modell bestimmen sollte
Trade-Anzahl geteilt durch Base-Volumen ergibt einen durchschnittlichen Print von 0,032 BTC, etwa 2.700 $. Wenn deine Kandidaten-Strategie 250.000 $ auf einmal eröffnen will, verlangt sie damit, ungefähr 92-mal so groß zu sein wie die typische Transaktion in dieser Minute. Diese Zahl, nicht irgendeine generische Slippage-Konstante von 5 Basispunkten, sollte deinen Square-Root-Impact-Term treiben. Wir berechnen die Participation Rate pro Kerze als eigenständige Spalte und verwerfen Strategien, deren mediane Order-Größe beim Einstieg ein paar Prozent des Bar-Notionals übersteigt, weil alles danach Fiktion ist.
Die Trade-Anzahl markiert auch billig die seltsamen Minuten. Volumen normal, Trade-Anzahl auf 11 eingebrochen? Da hat jemand einen Block gemacht. Volumen normal, Trade-Anzahl bei 9.000? Das ist eine Liquidation-Cascade, die in winzigen Häppchen durchgekaut wird.
[9] und [10]: "21.883", das Feld, das jeder wegwirft
Taker-Buy Base-Volumen. 21,883 von 38,417 BTC in dieser Kerze waren käuferinitiert, das Signed Volume Delta ist also +5,349 BTC und der Aggressor-Split liegt bei 57/43 zugunsten der Käufer. Die Börse liefert dir Order-Flow-Imbalance, kostenlos, in einem Feld, das die meisten Leute nie lesen, weil pandas ihnen die Spalte nicht benannt hat.
Ich behaupte nicht, dass es allein Returns vorhersagt; naive Delta-Strategien gehören zu den zuverlässigeren Wegen, an das Fee-Ledger zu spenden. Aber es ist eine wirklich andere Messung als der Preis, es steht in derselben Anfrage zur Verfügung, die du sowieso schon gestellt hast, und es lässt dich einen Rally unterscheiden, der gekauft wurde, von einem Rally, der entstand, weil sich Verkäufer zurückgezogen haben. Diese beiden sehen im OHLC identisch aus und verhalten sich zehn Minuten später unterschiedlich. Unser Research-Agent behandelt eine Hypothese, die den Taker-Split auf einer Venue ignoriert, die ihn veröffentlicht, als liegengelassene Evidenz.
[11] "0": ignore – und alles andere, was hier fehlt
Index 11 ist ein deprecated Feld, dauerhaft null. Interessanter ist die Liste dessen, was diese Kerze nicht enthält: kein Bid, kein Ask, kein Spread, keine Book-Tiefe, keine Funding-Rate, kein Open Interest, keine Liquidationen, kein Mark-Preis, kein Index-Preis. Und entscheidend: keine Möglichkeit zu wissen, ob deine Order Maker oder Taker gewesen wäre – der Unterschied zwischen 0,045 % zahlen und 0,01 % verdienen auf dieser Venue.
Jedes Fee-Modell, das allein auf Klines aufbaut, ist also eine Annahme in den Kleidern einer Zahl. Wir lösen das, indem wir jede Strategie zwingen, ihren Ausführungsstil vorab zu deklarieren, und dann Taker auf alles verrechnen, was nichts anderes belegen kann.
Die Kerze, die nie auftauchte
Letztes Stück, und das, das außerhalb der Majors am härtesten zubeißt. Eine Minute ohne Trades erzeugt überhaupt keine Kline. Datenanbieter und Bibliotheken forward-füllen das häufig: Open = High = Low = Close = vorheriger Close, Volumen 0. Dein Indikator berechnet fröhlich weiter. Deine Strategie sieht eine gültige Zeile und kann ein Signal für eine Minute erzeugen, in der niemand auf der Welt dieses Instrument gehandelt hat.
Bei einem Mid-Cap-Perp, den wir eingelesen haben, hatten 4,1 % der 1-Minuten-Kerzen in einem Zwölf-Monats-Fenster null Trades. Ein Mean-Reversion-Kandidat für dieses Symbol platzierte 38 % seiner Einstiege auf synthetischen Kerzen, weil flache synthetische Preise für alles, was Abweichung von einem gleitenden Durchschnitt misst, wie Katzenminze wirken. Er backtestete wunderschön. Er handelte die Lücken in den Daten.
Deshalb führt die Ingest-Schicht jetzt einen synthetic-Boolean pro Kerze, propagiert durch jedes Resampling, und der Verifikations-Parcours lässt jede Strategie durchfallen, deren Trades sich darauf konzentrieren. Billige Spalte. Sie hat mehr Kandidaten getötet als jeder Indikator, den wir je geschrieben haben.
Zwölf Werte. Vier davon routinemäßig falsch gelesen, zwei davon routinemäßig verworfen, und eine ganze Kategorie davon fehlt in der Zeile und wird von der Engine imaginiert. Bevor du deinen nächsten Backtest fährst, hol eine rohe Kerze aus deinem eigenen Store und lies jedes Feld laut vor, gegen das, was deine Fill-Logik darüber annimmt. Es ist eine Zwanzig-Minuten-Übung, und ich habe noch niemanden gesehen, der das gemacht und nichts gefunden hat.
← Alle Beiträge