4 september 2026 · backtesting

Både din stop och ditt mål låg inom samma stapel. Vilken valde din backtest?

Både din stop och ditt mål låg inom samma stapel. Vilken valde din backtest?

Lång från 84,120. Stop vid 84,036, mål vid 84,271. Nästa minutstapel kommer in: öppning 84,118, högsta 84,290, lägsta 84,010, stängning 84,240.

Båda nivåerna ligger inom stapeln. Stoppen träffades och målet träffades, och de fyra OHLC-värdena innehåller exakt noll information om vilket som kom först. Din backtest gav ändå ett resultat. Någonstans i loopen avgjorde en rad saken — förmodligen en rad som du inte tänkte på som ett modelleringsantagande när du skrev den.

Det här är den största enskilda källan till falsk avkastning jag ser i strategier med kort horisont, före avgifter och före slippage, eftersom det inte ser ut som ett antagande. Det ser ut som rörmokeri.

Fel sätt ett: låt if-kedjan avgöra

Det vanliga upplägget:

if bar.high >= target:
    exit(target, "tp")
elif bar.low <= stop:
    exit(stop, "sl")

Ingen valde ”mål löses före stoppar”. Målkontrollen skrevs bara in först eftersom det är den önskade vägen och den du hade i tankarna. Byt plats på grenarna så förändras aktiekurvan; bara det borde säga dig att strategins P&L delvis är en egenskap hos din kodredigerare.

Jag körde en medvetet ordinär mean-reversion-scalper på BTCUSDT perp, tre månader med 1-minutsstaplar, 4,812 affärer, stop 0.10% och mål 0.18% från ingången. Samma signaler, samma avgifter, bara konventionen för att avgöra tvetydiga fall ändrades.

12.6%av affärerna träffade båda nivåerna i en stapel
2.31Sharpe, mål löses först
0.18Sharpe, stoppar löses först
0.94Sharpe, löst med 1-sekundsstaplar

En åttondel av affärerna står för hela resultatet. Det är matematiken bakom ett tätt stop-och-mål-par: de tvetydiga affärerna är de där priset rörde sig åt båda hållen, vilket gäller de flesta intressanta fallen, och var och en är värd hela avståndet mellan stop och mål beroende på slantsinglingen. 12.6% av affärerna × 0.28% av spannet blir 3.5% av bruttoomsättningen i nominellt värde per urvalsenhet, vilket vida överstiger strategins faktiska edge.

Andelen beror på stapelstorleken i förhållande till avståndet mellan nivåerna och blir snabbt värre när staplarna blir grövre. Samma stop och mål, samma signaler, omsamplade:

StaplingsintervallAffärer med båda nivåerna inom en stapelSharpe (mål först)
1s0.3%0.91
1m12.6%2.31
5m34%3.60
15m49%4.42
1h71%5.88

Se vad tabellen faktiskt säger. Grövre staplar gjorde backtesten bättre. Varje forskares instinkt säger att timstaplar är det konservativa valet: mindre brus, mindre överanpassning till marknadsmikrostruktur. Men med en intrabar-konvention som löser fallen till din fördel är en grövre stapel bara en större låda där du får anta att du hade tur. Med 1h-staplar är sju av tio affärer rena konventionsfrågor. Den backtesten testar inte en strategi, utan ordningen på två `if`-satser 3,400 gånger.

Fel sätt två: anta att stoppen fylldes till stoppriset

Säg att du rättar till ordningen. Stoppen löses först, du bokför en förlust på exakt 0.10% plus taker-avgift och känner dig noggrann. Två separata saker är fortfarande fel.

Det första är att en stop är en utlösare, inte en fyllning. På Binance USDⓈ-M blir en STOP_MARKET-order en marknadsorder så fort utlösningsvillkoret uppfylls, och den tar sedan vad som finns i orderboken. Under en lugn minut innebär det en eller två ticks slippage. Under minuten då din stop faktiskt utlöstes — den med en 40-punkters ljuskropp och en likvideringskaskad under sig — är orderboken tunn på precis den sida du handlar mot. I mitt urval, där stopputlösningar matchades mot tickdatan, låg medianfyllningen 1.4 bps efter utlösningsnivån och den 95:e percentilen låg på 11 bps. Vid en stop på 10 bps kostar svansen dig ytterligare en tiondel av risken du trodde dig ha begränsat.

Det andra är subtilare och specifikt för perps: vilket pris som utlöser den. Binance använder som standard mark price för stopordrar, och mark price beräknas från indexet plus en utjämnad basis, inte från senaste affären på den handelsplatsen. Din OHLC-serie är senaste pris. Det är två olika serier, och de skiljer sig mest åt just under de händelser som utlöser stoppar.

Senaste pris (dina klines)Mark price (standardutlösare)
Källaaffärer på denna handelsplatsindex från flera handelsplatser + basis
Wick-beteendehela rörelsenkraftigt dämpad
Typisk avvikelse1–3 bps i lugnt läge, 20–35 bps under en kaskadminut
Konsekvens för backtestenstoppar som utlöstes fast de inte borde ha gjort det, och tvärtom

En 25 bps-wick i serien för senaste pris stoppar alltså ut dig i backtesten, medan livevärdet för mark price aldrig kom närmare än 10 bps från din utlösningsnivå. Eller tvärtom, den dag indexet rör sig och din handelsplats släpar efter. Om du ställer in workingType till CONTRACT_PRICE matchar du åtminstone livebeteendet mot dina data, och för en forskare är det vanligtvis rätt val, eftersom en ärlig simulering av en mark price-utlösare kräver att du för med dig en andra serie genom hela din fyllningsmotor.

Den här versionen minns jag bäst: någon på vår sida ”förbättrade” en strategi genom att flytta take-profit från 0.18% till 0.21%. Sharpe gick från 2.3 till 3.1. Ingen ny edge. Målet hade helt enkelt flyttats utanför den tjockaste delen av 1-minutsfördelningen för wickar, så färre affärer hamnade i den tvetydiga grupp där koden i tysthet tilldelade dem vinsten. De hade optimerat utslagsregeln.

Fel sätt tre: anta alltid det värsta och kalla det konservativt

Den spontana lösningen är pessimism. Om båda nivåerna träffas, ta stoppen. Klart, ingen mer optimism, kör.

Jag brukade göra så. Det är bättre än alternativet, men fortfarande fel, av två skäl.

Det sållar bort strategier som fungerar. En pessimistisk lösning för 12.6% av affärerna kostade den här strategin 2.1 Sharpe-poäng jämfört med 0.94 när fallen löstes med tickdata. Om det sanna värdet är 0.94 och din konvention visar 0.18 slänger du bort idén och går vidare till något sämre. Konservatism som slår fel med två Sharpe-poäng är inte konservatism, det är brus med en moralisk hållning.

Värre är att den förvränger optimeringen. Ge en parametersvepning en pessimistisk utslagsregel så lär sig optimeraren att undvika tvetydigheter, eftersom tvetydighet nu innebär ett rent straff. Den dras mot breda stoppar och nära mål, eller mot långsamma staplar där de två nivåerna sällan sammanfaller, och presenterar parametrar som valts utifrån din fyllningskonvention i stället för marknaden. Samma fel som med den generösa varianten, fast med motsatt tecken, och lika osynligt i resultatrapporten.

Tumregel vi använder innan vi gör något annat: om stop_distance + target_distance är mindre än 75:e percentilens spann för ditt staplingsintervall, är ditt intrabar-antagande en större faktor i P&L än din signal. Räkna ut båda värdena. Det tar fyra rader kod och har avslutat fler strategigranskningar än någon annan enskild kontroll.

Vad som faktiskt fungerar

Prisets väg inom stapeln är data. Hämta den, eller avgränsa det du inte kan få fram.

  1. Lös fallen med den finaste tidsserien du har. Binance aggTrades för de berörda minuterna omfattar några hundra rader och avgör frågan direkt: vilken nivå träffades först och till vilket pris sweepen fylldes. Du behöver inte tickdata för hela backtesten, bara för de tvetydiga staplarna. I mitt urval handlade det om 606 minuter av 129,600. Det är en liten nedladdning, inget infrastrukturprojekt.
  2. Om tickdata saknas, gå ned ett eller två tidsintervall bara för att lösa fallen. Signaler på 15m, utgångar lösta med 1s- eller 1m-staplar. Tvetydigheten sjunker från 49% till en bråkdel av en procent, och resten är så liten att den går att bortse från med gott samvete.
  3. Redovisa alltid spannet. Kör varje backtest två gånger, med optimistisk och pessimistisk lösning, och visa båda Sharpe-värdena bredvid det lösta värdet. Skillnaden är din intrabar-osäkerhet och hör hemma i resultatrapporten bredvid konfidensintervallet för själva Sharpe-värdet. När spannet är 0.2–2.3 är ingen slutsats inom det verklig.
  4. Följ tvetydighetsgraden som ett nyckeltal i sig. Vår visas högst upp på varje strategikort, bredvid antalet affärer och omsättningen. En andel över ungefär 5% betyder att det är utgångslogiken, inte ingångslogiken, som testas.
  5. Modellera utlösaren separat från fyllningen. Använd den prisserie handelsplatsen faktiskt utgår från när ordern utlöses; fyll till utlösningspris plus slippage dragen från en fördelning kalibrerad mot tickdatan, inte till själva utlösningspriset.

Aktier har samma problem i annan skepnad. En stop på 62.00 i en aktie som öppnar med gap på 58.40 över natten fylls inte på 62.00, utan någonstans under öppningspriset. En backtest med dagsstaplar som bokför −$0.00 i slippage vid gap förbi stopnivån kommer glatt att säga att ett stop-loss-skydd förbättrade din drawdown. Det gjorde det inte. Det testades bara aldrig de dagar som spelar roll. Handelsstopp ger samma problem: det är i återöppningsauktionen som din stop faktiskt löses, till ett pris som stapelns lägsta värde aldrig visar.

Inget av det här är exotiskt. Det handlar om att inse att en stapel är en sammanfattning och att en stop-och-mål-strategi är ett vad på händelsernas ordningsföljd, som sammanfattningen kastade bort. När paper trading-motorn till slut kör strategin mot en live-tape har tapen en egen uppfattning om ordningsföljden, och den har aldrig brytt sig om vilken gren i if-satsen du skrev först.

intrabar-exekveringarstopordrarbacktestingkryptofuturesmarknadsmikrostruktur
← Alla inlägg