Her er én bar. Binance USDⓈ-M perpetual, BTCUSDT, minuttet der begynder 13:46:00 UTC. Den kom fra klines-endpointet som et rent array med tolv værdier, og det er den mest almindelige input-enhed i retail-og-tilstødende quant-research. Stort set enhver strategi, vi genererer, rører ved noget, der ser sådan ud.
Så lad os skille den ad, indeks for indeks, og se hvor meget af den et backtest får galt.
| Indeks | Værdi | Navn |
|---|---|---|
| 0 | 1773495960000 | åbningstid (ms) |
| 1 | "84120.50" | åbning |
| 2 | "84177.90" | højeste |
| 3 | "84098.10" | laveste |
| 4 | "84163.40" | lukning |
| 5 | "38.417" | basisvolumen (BTC) |
| 6 | 1773496019999 | lukketid (ms) |
| 7 | "3232108.94" | kvotevolumen (USDT) |
| 8 | 1204 | antal handler |
| 9 | "21.883" | taker buy basisvolumen |
| 10 | "1841203.55" | taker buy kvotevolumen |
| 11 | "0" | ignorer |
[0] og [6]: hvilket minut er dette, og hvis ur siger det
Åbningstid 1773495960000, lukketid 1773496019999. Læg mærke til den sidste: den ender på 19999, ét millisekund kortere end næste bars åbning. Vinduet er halvåbent, og børsen fortæller dig det eksplicit. Halvdelen af de databugs, jeg har jagtet, startede med, at nogen behandlede begge endepunkter som inklusive og talte en handel dobbelt ved grænsen, eller justerede en resampling, så hver 5-minutters bar lånte et millisekund fra sin nabo.
Tidsstemplerne er børsens matching-engine-tid. Ikke dit eget ur, ikke din leverandørs ingestion-ur, og ikke det samme ur, som funding-snapshottet bruger. Når du joiner en kline-serie med en funding-rate-serie eller en open-interest-serie hentet fra et andet endpoint, joiner du på tværs af kilder, der som regel kun er enige inden for cirka et sekund. For en 1-minuts strategi giver det en timingfejl på 1,7% i hver joinet række. For alt under et minut er det fatalt.
Og denne bar er tidsstemplet ved sin åbning. Informationen i den kan ikke kendes før 13:46:59.999. Ethvert spørgsmål om indekseringskonvention — skal jeg forskyde med én, skal jeg bruge bar-close- eller bar-open-tidsstempler som mit event-tidspunkt — er i virkeligheden det samme lookahead-spørgsmål i forklædning.
[1] "84120.50": åbningen du ikke kan handle på
Åbningen er prisen på den første handel, der blev printet inden for vinduet. Det er en gennemført transaktion mellem to andre parter. I det øjeblik baren eksisterer som en række i din dataframe, er den pris tres sekunder gammel.
Den forrige bar lukkede på 84109.80, så der er $10.70 i prisbevægelse gemt i gabet mellem to tilstødende 1-minuts barer. Det er 1,3 basispoint, usynligt på et chart, og cirka en fjerdedel af taker-gebyret. Fint nok. Men gå ud og kig på den samme serie på en alt-perp under en amerikansk CPI-udmelding, og de gab mellem barer løber på 15 til 40 bps. Et backtest, der genererer signal ved bar-close og fylder ordren ved næste bars open, antager stiltiende, at gabet er nul — og det er kun nul, når det ikke betyder noget.
[2] og [3]: "84177.90" højeste, "84098.10" laveste
Det er her, de fleste fill-engines går til grunde, så dette afsnit er det lange.
Højeste og laveste er ekstreme priser, der blev touched. De har hverken størrelse eller varighed. Fra den rå trade-tape for det samme minut var alt på eller under 84100.00 i alt 0,62 BTC fordelt på ni prints inden for et vindue på 1,4 sekunder. Så et backtest-stop på 84100 "fylder" — og hvis din position er 3 BTC, spiste du hele den synlige bog på vej ned, og resten af din størrelse blev fyldt et sted i tilbagestuds-intervallet 84105–84130. Baren siger, at laveste var 84098.10. Baren siger ikke, at kun $52.000 i notional blev handlet der.
Den anden retning er værre, fordi den smigrer dig. Antag, du havde en hvilende limit-sell på 84175. Barens højeste er 84177.90, så en naiv engine fylder dig på 84175 og bogfører maker-rabat i stedet for taker-omkostning. Om den fill rent faktisk skete, afhænger af køposition på det prisniveau, hvilket baren ikke kan vide, og som du sandsynligvis aldrig registrerede. Touched er ikke det samme som filled.
Reglen, vi landede på i vores fill-engine: en hvilende limit fyldes kun, hvis baren handler igennem niveauet, ikke blot op til det. Fills på den eksakte ekstremtick kræver volume-at-price-belæg fra trade-tapen, ellers afvises de. Det fjernede cirka 6% af handlerne fra en typisk mean-reversion-bog og skar en kandidats backtestede Sharpe fra 1,9 til 1,1. Den kandidat var aldrig reel; fill-reglen var bare det første, der var ærligt nok til at sige det.
Beslægtet fælde: intrabar-forløbet. Hvis en bars range spænder over både dit stop og din take-profit, kan OHLCV ikke fortælle dig, hvad der kom først. Enhver engine er nødt til at vælge en konvention. Vores antager altid stop-først, hvilket er pessimistisk og af og til forkert, men aldrig genererer en falsk gevinst. Hvis din engine antager take-profit-først, vil brede barer fabrikere profit ud af tvetydighed, og brede barer er netop dem, der dominerer din PnL-fordeling.
[4] "84163.40": det mindst robuste tal i baren
Lukningen er det sidste print i vinduet. Det er det. Det kan være et 0,002 BTC odd-lot fra en bot, der runder en position af klokken 13:46:59.8. Denne enkelte, strukturelt vilkårlige tick er det, de fleste research-pipelines bruger til at beregne hvert signal, marke hver position og evaluere hver exit.
På BTC-perps betyder det næsten ingenting; på en tynd altcoin-perp klokken 04:00 UTC betyder det enormt meget, og forskellen mellem to venues' lukninger for det samme minut kan overstige hele din edge per trade. Når en strategis PnL afhænger specifikt af lukningen, kører vi den igen med mark-pris fra børsen i stedet, som er indeksafledt og langt sværere at manipulere. Hvis resultaterne divergerer, handlede strategien artefaktet.
[5] og [7]: "38.417" og "3232108.94", volumen målt i hvad
Basisvolumen er BTC; kvotevolumen er USDT. Begge dele her, hvilket er en imødekommenhed, ikke alle venues yder. Grunden til at det er vigtigt, er aggregering på tværs af venues. Coin-marginerede kontrakter noteres i kontrakter af $100 notional. Nogle equity-feeds rapporterer round lots. Prediction-market-venues rapporterer share-antal, hvor en share er et dollardenomineret binært krav. At summere "volumen" på tværs af et blandet univers uden at normalisere til én fælles notional-enhed giver en likviditetsrangering, der er det rene nonsens — og det vil være nonsens på en stabil-udseende måde, der overlever review.
Normaliser alt til kvote-notional ved ingest. Gem også råfeltet, men lad aldrig en strategi se det.
[8] 1204: feltet der burde styre din impact-model
Antal handler divideret op i basisvolumen giver et gennemsnitligt print på 0,032 BTC, cirka $2.700. Hvis din kandidatstrategi vil ind med $250.000 på én gang, beder den om at være cirka 92 gange størrelsen af den typiske transaktion i det minut. Det tal — ikke en generisk 5-bps slippage-konstant — er det, der bør drive dit kvadratrod-impact-led. Vi beregner deltagelsesrate per bar som en førsteklasses kolonne og afviser strategier, hvis medianindgang overstiger nogle få procent af barens notional, fordi alt nedstrøms fra det er fiktion.
Antal handler flager også de mærkelige minutter billigt. Normal volumen, men antal handler kollapset til 11? Nogen lavede en blokhandel. Normal volumen, men antal handler på 9.000? Det er en likvidationscascade, der bliver tygget igennem i bittesmå stykker.
[9] og [10]: "21.883", feltet alle smider væk
Taker buy basisvolumen. 21,883 af 38,417 BTC i denne bar var køberinitieret, så den signerede volumendelta er +5,349 BTC, og aggressor-fordelingen er 57/43 mod køberne. Børsen giver dig order flow imbalance, gratis, i et felt de fleste aldrig læser, fordi pandas ikke navngav kolonnen for dem.
Jeg påstår ikke, at det forudsiger afkast alene; naive delta-strategier er blandt de mere pålidelige måder at donere til gebyrregnskabet på. Men det er en reelt anderledes måling end pris, det er tilgængeligt i den samme request, du allerede lavede, og det lader dig skelne mellem et rally, der blev købt, og et rally, der skete, fordi sælgerne trak sig. De to ser identiske ud i OHLC og opfører sig forskelligt ti minutter senere. Vores research-agent behandler en hypotese, der ignorerer taker-fordelingen på en venue, der udgiver den, som at efterlade beviser på bordet.
[11] "0": ignorer — og alt det andet, der ikke er her
Indeks 11 er et udfaset felt, permanent nul. Mere interessant er listen over ting, denne bar ikke indeholder: ingen bid, ingen ask, ingen spread, ingen book-dybde, ingen funding rate, ingen open interest, ingen likvidationer, ingen mark-pris, ingen indekspris. Og afgørende: ingen måde at vide, om din ordre ville have været maker eller taker, hvilket er forskellen mellem at betale 0,045% og tjene 0,01% på denne venue.
Så enhver gebyrmodel bygget udelukkende på klines er en antagelse iklædt et tals tøj. Vi løser det ved at tvinge enhver strategi til at deklarere sin eksekveringsstil på forhånd, og derefter opkræve taker-gebyr på alt, der ikke kan bevise det modsatte.
Baren, der aldrig dukkede op
Sidste stykke, og det der bider hårdest uden for majors. Et minut uden handler producerer slet ingen kline. Leverandører og biblioteker forward-filler det typisk: open = high = low = close = forrige lukning, volumen 0. Din indikator beregner glad videre. Din strategi ser en gyldig række og kan generere et signal på et minut, hvor ingen i hele verden handlede det instrument.
På en mid-cap-perp, vi indfødte, havde 4,1% af 1-minuts barerne i et tolv-måneders vindue nul handler. En mean-reversion-kandidat på det symbol placerede 38% af sine indgange på syntetiske barer, fordi flade syntetiske priser er som kattemynte for alt, der måler afvigelse fra et glidende gennemsnit. Den backtestede smukt. Den handlede huller i dataene.
Det er derfor, ingest-laget nu bærer en synthetic-boolean per bar, videreført gennem hver resampling, og verifikationsgauntlet'en underkender enhver strategi, hvis handler klumper sig på den. Billig kolonne. Den har dræbt flere kandidater end nogen indikator, vi nogensinde har skrevet.
Tolv værdier. Fire af dem bliver rutinemæssigt fejllæst, to af dem bliver rutinemæssigt smidt væk, og en hel kategori er fraværende fra rækken og bliver opdigtet af engine'en. Før dit næste backtest, hent én rå bar fra dit eget lager og læs hvert felt højt op imod, hvad din fill-logik antager om det. Det er en tyve-minutters øvelse, og jeg har aldrig set nogen gøre det og ikke finde noget.
← Alle indlæg