26 augusti 2026 · marknadsdata

Anatomin hos en 1-minutersstapel: allt OHLCV inte berättar för din backtest

Anatomin hos en 1-minutersstapel: allt OHLCV inte berättar för din backtest

Här är en stapel. Binance USDⓈ-M perpetual, BTCUSDT, minuten som börjar 13:46:00 UTC. Den kom från klines-endpointen som en ren array med tolv värden, och det är den enskilt vanligaste inputenheten inom retail-närliggande kvantforskning. Nästan varje strategi vi genererar rör vid något format så här.

Så låt oss plocka isär den, index för index, och se hur mycket av den en backtest får fel.

IndexVärdeNamn
01773495960000öppningstid (ms)
1"84120.50"öppning
2"84177.90"högsta
3"84098.10"lägsta
4"84163.40"stängning
5"38.417"volym i basvaluta (BTC)
61773496019999stängningstid (ms)
7"3232108.94"volym i noteringsvaluta (USDT)
81204antal avslut
9"21.883"taker-köpvolym i basvaluta
10"1841203.55"taker-köpvolym i noteringsvaluta
11"0"ignorera

[0] och [6]: vilken minut är detta, och vems klocka säger det

Öppningstid 1773495960000, stängningstid 1773496019999. Lägg märke till den andra: den slutar på 19999, en millisekund kort av nästa stapels öppning. Fönstret är halvöppet, och börsen talar uttryckligen om det för dig. Hälften av de databuggar jag har jagat började med att någon behandlade båda ändpunkterna som inkluderande och dubbelräknade en handel vid gränsen, eller justerade en resampling så att varje 5-minutersstapel lånade en millisekund från sin granne.

Tidsstämplarna är börsens matchningsmotortid. Inte din klocka, inte din leverantörs inhämtningsklocka, och inte samma klocka som funding-snapshotten använder. När du kopplar samman en klineserie med en funding rate-serie eller en open interest-serie hämtad från en annan endpoint kopplar du samman källor som stämmer överens inom ungefär en sekund, för det mesta. För en 1-minutersstrategi är det ett tidsfel på 1.7% i varje sammanfogad rad. För allt under en minut är det ödesdigert.

Och den här stapeln är stämplad vid sin öppning. Informationen i den går inte att känna till förrän 13:46:59.999. Varje fråga om indexeringskonvention — ska jag skifta med ett, ska jag använda bar-close- eller bar-open-tidsstämplar som min händelsetid — är i själva verket samma lookahead-fråga i förklädnad.

[1] "84120.50": öppningen du inte kan handla

Öppningen är priset för den första handel som avslutades inom fönstret. Det är en genomförd transaktion mellan två andra parter. När stapeln väl existerar som en rad i din dataframe är det priset sextio sekunder gammalt.

Föregående stapel stängde på 84109.80, så det finns $10.70 av prisrörelse i gapet mellan två intilliggande 1-minutersstaplar. Det är 1.3 punkter (bps), osynligt på ett diagram, och ungefär en fjärdedel av taker-avgiften. Visst. Men titta på samma serie för en alt-perp under en amerikansk KPI-rapport, och de där gapen mellan staplar ligger på 15 till 40 bps. En backtest som signalerar vid stapelns stängning och fyller vid nästa stapels öppning antar i tysthet att gapet är noll, och det är noll precis när det inte spelar någon roll.

[2] och [3]: "84177.90" högsta, "84098.10" lägsta

Dessa två är där de flesta fill-motorer går för att dö, så det här avsnittet är det långa.

Högsta och lägsta är extrema priser som berörts. De bär ingen storlek och ingen varaktighet. Från den råa handelsloggen för samma minut var allt vid eller under 84100.00 totalt 0.62 BTC fördelat på nio avslut inom ett fönster på 1.4 sekunder. Så en backteststop vid 84100 "fylls" — och om din position är 3 BTC åt du upp hela den synliga orderboken på vägen ner, och resten av din storlek gjordes klar någonstans i återgången 84105–84130. Stapeln säger att lägsta var 84098.10. Stapeln säger inte att bara $52,000 i notionellt värde handlades där.

1204avslut under minuten
0.032 BTCgenomsnittlig avslutsstorlek (~$2.7k)
0.62 BTCtotalt handlat vid eller under lägsta tick
57%av volymen var köparinitierad

Den andra riktningen är värre, eftersom den smickrar dig. Anta att du hade en vilande limit-sälj på 84175. Stapelns högsta är 84177.90, så en naiv motor fyller dig på 84175 och bokför maker-rabatt istället för taker-kostnad. Om den fyllningen faktiskt inträffade beror på köposition vid den prisnivån, vilket stapeln inte kan veta och som du förmodligen aldrig registrerade. Berört är inte detsamma som fyllt.

Regeln vi landade i för vår fill-motor: en vilande limit fylls bara om stapeln handlar igenom nivån, inte bara till den. Fyllningar vid exakt den extrema ticken kräver bevis på volym-vid-pris från handelsloggen, annars avvisas de. Det tog bort ungefär 6% av avsluten från en typisk mean-reversion-portfölj och sänkte en kandidats backtestade Sharpe från 1.9 till 1.1. Den kandidaten var aldrig verklig; fill-regeln var bara det första som var ärligt nog att säga det.

Relaterad fälla: intrabar-vägen. Om en stapels spann omfattar både din stop och ditt take-profit kan OHLCV inte tala om för dig vilket som kom först. Varje motor måste välja en konvention. Vår antar stop-först, alltid, vilket är pessimistiskt och ibland fel men aldrig genererar en falsk vinst. Om din motor antar take-profit-först kommer staplar med brett spann att tillverka vinst ur oklarhet, och staplar med brett spann är exakt de som dominerar din PnL-fördelning.

[4] "84163.40": den minst robusta siffran i stapeln

Stängningen är det sista avslutet i fönstret. Det är allt. Det kan vara en udda post på 0.002 BTC från en bot som avrundar en position vid 13:46:59.8. Denna enda, strukturellt godtyckliga tick är vad de flesta forskningspipelines använder för att beräkna varje signal, marka varje position och utvärdera varje exit.

På BTC-perps spelar det knappt någon roll; på en tunn altcoin-perp klockan 04:00 UTC spelar det enorm roll, och skillnaden mellan två plattformars stängningar för samma minut kan överstiga hela din edge per handel. När en strategis PnL beror specifikt på stängningen kör vi om den och markerar mot börsens mark price istället, vilket är indexhärlett och betydligt svårare att peta i. Om resultaten skiljer sig åt handlade strategin artefakten.

[5] och [7]: "38.417" och "3232108.94", volym i vilka enheter

Basvolym är BTC; quote-volym är USDT. Båda här, vilket är en artighet som inte alla plattformar erbjuder. Anledningen att bry sig är aggregering över plattformar. Coin-marginerade kontrakt noteras i kontrakt om $100 i notionellt värde. Vissa aktiedataflöden rapporterar jämna poster. Prediction market-plattformar rapporterar andelsantal där en andel är ett dollardenominerat binärt anspråk. Att summera "volym" över ett blandat universum utan att normalisera till en enda notionell enhet ger en likviditetsrankning som är rent nonsens, och det kommer att vara nonsens på ett stabilt utseende sätt som överlever granskning.

Normalisera allt till quote-notionellt värde vid inhämtning. Spara även rådatafältet, men låt aldrig en strategi se det.

[8] 1204: fältet som borde styra din impact-modell

Antal avslut delat med basvolym ger ett genomsnittligt avslut på 0.032 BTC, cirka $2,700. Om din kandidatstrategi vill gå in med $250,000 på en gång ber den om att vara ungefär 92 gånger storleken på den typiska transaktionen under den minuten. Den siffran, inte någon generisk slippage-konstant på 5 bps, är vad som borde driva din kvadratrotsterm för impact. Vi beräknar deltagandegrad per stapel som en förstklassig kolumn och avvisar strategier vars mediana inträde överstiger några få procent av stapelns notionella värde, eftersom allt nedströms från det är fiktion.

Antalet avslut flaggar också billigt de konstiga minuterna. Normal volym, men antalet avslut kollapsat till 11? Någon gjorde en block-affär. Normal volym, men antalet avslut på 9,000? Det är en likvidationskaskad som tuggas igenom i små bitar.

[9] och [10]: "21.883", fältet alla kastar bort

Taker-köpvolym i basvaluta. 21.883 av 38.417 BTC i denna stapel var köparinitierad, så det signerade volymdeltat är +5.349 BTC och aggressorfördelningen är 57/43 mot köpare. Börsen ger dig order flow imbalance, gratis, i ett fält som de flesta aldrig läser eftersom pandas inte namngav kolumnen åt dem.

Jag påstår inte att det förutsäger avkastning på egen hand; naiva delta-strategier är bland de mer pålitliga sätten att donera till avgiftsboken. Men det är ett genuint annorlunda mått än pris, det är tillgängligt i samma request du redan gjorde, och det låter dig skilja en rally som köptes från en rally som skedde för att säljare drog sig undan. De två ser identiska ut i OHLC och beter sig olika tio minuter senare. Vår forskningsagent behandlar en hypotes som ignorerar taker-fördelningen på en plattform som publicerar den som att lämna bevis på bordet.

[11] "0": ignorera — och allt annat som inte finns här

Index 11 är ett föråldrat fält, permanent noll. Mer intressant är listan över saker denna stapel inte innehåller: inget bud, inget säljbud, ingen spread, inget orderboksdjup, ingen funding rate, ingen open interest, inga likvidationer, inget mark price, inget indexpris. Och avgörande: inget sätt att veta om din order skulle ha varit maker eller taker, vilket är skillnaden mellan att betala 0.045% och att tjäna 0.01% på denna plattform.

Så vilken avgiftsmodell som helst byggd enbart på klines är ett antagande klätt i en siffras kläder. Vi löser det genom att tvinga varje strategi att deklarera sin exekveringsstil i förväg, och sedan ta ut taker-avgift på allt som inte kan bevisa motsatsen.

Stapeln som aldrig dök upp

Sista biten, och den som biter hårdast utanför de stora paren. En minut med noll avslut ger ingen kline alls. Leverantörer och bibliotek forward-fyller den vanligtvis: öppning = högsta = lägsta = stängning = föregående stängning, volym 0. Din indikator beräknar glatt. Din strategi ser en giltig rad och kan generera en signal på en minut då ingen i hela världen handlade det instrumentet.

På en mid-cap-perp vi hämtade in hade 4.1% av 1-minutersstaplarna under ett tolvmånadersfönster noll avslut. En mean-reversion-kandidat på den symbolen lade 38% av sina inträden på syntetiska staplar, eftersom platta syntetiska priser är kattmynta för allt som mäter avvikelse från ett glidande medelvärde. Den backtestade fantastiskt. Den handlade luckorna i datan.

Det är därför inhämtningslagret nu bär en synthetic-boolean per stapel, som propageras genom varje resampling, och verifieringsgauntleten underkänner varje strategi vars avslut klustrar sig kring den. Billig kolumn. Den har dödat fler kandidater än någon indikator vi någonsin skrivit.

Tolv värden. Fyra av dem rutinmässigt feltolkade, två av dem rutinmässigt bortkastade, och en hel kategori av dem frånvarande i raden och inbillad av motorn. Innan din nästa backtest, hämta en rå stapel från din egen lagring och läs upp varje fält högt mot vad din fill-logik antar om det. Det är en tjugominutersövning och jag har aldrig sett någon göra det och inte hitta något.

ohlcvmarknadsmikrostrukturdataengineeringbacktestningkryptoterminer
← Alla inlägg