Long vanaf 84,120. Stop op 84,036, doel op 84,271. De candle van de volgende minuut komt binnen: open 84,118, high 84,290, low 84,010, close 84,240.
Beide niveaus liggen binnen die candle. Zowel de stop als het doel werd geraakt, en de 4 OHLC-getallen bevatten precies 0 informatie over wat eerst gebeurde. Toch gaf je backtest een getal terug. Ergens in de lus heeft een regel de keuze gemaakt — waarschijnlijk een regel die je bij het schrijven ervan niet als modelaanname zag.
Dit is de grootste bron van nepresultaten die ik zie bij strategieën met een korte horizon, groter dan kosten en groter dan slippage, omdat het niet aanvoelt als een aanname. Het lijkt gewoon onderdeel van de afhandeling.
Verkeerde aanpak 1: de if-keten laten beslissen
Meestal ziet dat er zo uit:
if bar.high >= target:
exit(target, "tp")
elif bar.low <= stop:
exit(stop, "sl")
Niemand heeft gekozen voor ‘doelen worden afgehandeld vóór stops’. De controle op het doel werd gewoon als eerste getypt, omdat dat het verwachte pad is en het pad waar je aan dacht. Draai de volgorde van de 2 vertakkingen om en de vermogenscurve verandert; dat zou je al moeten vertellen dat de P&L van je strategie deels afhangt van de volgorde waarin je code schreef.
Ik draaide een bewust doorsnee scalper voor mean reversion op BTCUSDT perp, met 3 maanden aan candles van 1 minuut en 4,812 transacties, een stop op 0.10% en een doel op 0.18% vanaf de instap. Dezelfde signalen en kosten; alleen de conventie voor het doorbreken van gelijke uitkomsten veranderde.
Een achtste van de transacties bepaalt het hele resultaat. Dat is de rekensom bij een krap paar van stop en doel: de onduidelijke transacties zijn die waarbij de koers beide kanten op ging, en dat zijn juist de interessantste. Afhankelijk van de muntworp is elke transactie het volledige verschil tussen stop en doel waard. 12.6% van de transacties × een bereik van 0.28% is 3.5% van de bruto notionele omzet per eenheid van de steekproef, veel meer dan het werkelijke voordeel van de strategie.
Het percentage hangt af van de candleduur ten opzichte van de afstand tussen je niveaus, en loopt snel op naarmate je candles langer worden. Dezelfde stop en hetzelfde doel, dezelfde signalen, opnieuw bemonsterd:
| Candle-interval | Transacties met beide niveaus binnen 1 candle | Sharpe (doel eerst) |
|---|---|---|
| 1s | 0.3% | 0.91 |
| 1m | 12.6% | 2.31 |
| 5m | 34% | 3.60 |
| 15m | 49% | 4.42 |
| 1h | 71% | 5.88 |
Kijk wat die tabel eigenlijk zegt. Grovere candles maakten de backtest beter. Elke onderzoeker denkt al snel dat candles van 1 uur de voorzichtige keuze zijn: minder ruis, minder overfitting aan marktstructuur. Maar met een intrabar-conventie die in je voordeel uitvalt, is een grovere candle gewoon een grotere doos waarin je kunt aannemen dat je geluk had. Op candles van 1 uur zijn 7 van de 10 transacties puur afhankelijk van de conventie. Die backtest test geen strategie, maar de volgorde van 2 `if`-statements, 3,400 keer.
Verkeerde aanpak 2: aannemen dat de stop precies op de stopprijs is uitgevoerd
Stel dat je de volgorde vastlegt. De stop wordt als eerste afgehandeld, je boekt een verlies van precies 0.10% plus taker-kosten en je vindt dat je zorgvuldig te werk bent gegaan. Toch zijn er nog 2 dingen mis.
Ten eerste is een stop een trigger, geen uitvoering. Op Binance USDⓈ-M wordt een STOP_MARKET-order een marktorder zodra aan de trigger voorwaarde is voldaan. Daarna neemt die order wat er op dat moment in het orderboek staat. In een rustige minuut levert dat 1 of 2 ticks slippage op. In de minuut waarin je stop daadwerkelijk werd getriggerd — die met een candlebody van 40 punten en een liquidatiecascade eronder — is het orderboek juist dun aan de kant waar je tegenin handelt. In mijn steekproef, waarbij ik stoptriggers koppelde aan de tickdata, lag de mediane uitvoering 1.4 bps voorbij de trigger en het 95e percentiel op 11 bps. Bij een stop van 10 bps kost die uitschieter je nog eens een tiende van het risico dat je dacht te hebben vastgelegd.
Het tweede punt is subtieler en specifiek voor perps: welke prijs de trigger activeert. Binance stelt stoporders standaard in op mark price, die wordt berekend op basis van de index plus een afgevlakte basis, en niet op basis van de laatste transactie op dat handelsplatform. Je OHLC-reeks gebruikt de laatste prijs. Dat zijn verschillende reeksen, en ze lopen juist tijdens de gebeurtenissen die stops activeren het verst uiteen.
| Laatste prijs (je klines) | Mark price (standaardtrigger) | |
|---|---|---|
| Bron | transacties op dit handelsplatform | index van meerdere handelsplatforms + basis |
| Wick-gedrag | volledige uitslag | sterk afgevlakt |
| Typische afwijking | 1–3 bps bij kalm weer, 20–35 bps tijdens een cascade-minuut | |
| Gevolg voor de backtest | stops die wel afgingen maar niet hadden moeten afgaan, en andersom | |
Een wick van 25 bps in de reeks met laatste prijzen kan je backtest dus uit je positie stoppen, terwijl de live mark price nooit binnen 10 bps van je trigger kwam. Of het omgekeerde, op een dag waarop de index beweegt en je handelsplatform achterloopt. Als je workingType instelt op CONTRACT_PRICE, komt het live gedrag ten minste overeen met je data. Voor een onderzoeker is dat meestal de juiste keuze, want een mark-price-trigger eerlijk simuleren betekent dat je een tweede reeks door je volledige uitvoeringsengine moet voeren.
De versie die me het best is bijgebleven: iemand bij ons ‘verbeterde’ een strategie door de take-profit van 0.18% naar 0.21% te verplaatsen. De Sharpe steeg van 2.3 naar 3.1. Geen nieuw voordeel. Het doel was simpelweg buiten het dichtste deel van de wick-verdeling op candles van 1 minuut komen te liggen, waardoor minder transacties in de onduidelijke categorie vielen waar de code ze stilletjes de winst toekende. Diegene had de beslisregel geoptimaliseerd.
Verkeerde aanpak 3: altijd uitgaan van het slechtste en dat voorzichtig noemen
De reflexmatige oplossing is pessimisme. Als beide niveaus worden geraakt, neem je de stop. Klaar, geen optimisme meer, lanceren maar.
Vroeger deed ik dit ook. Het is beter dan het alternatief, maar nog steeds verkeerd, om 2 redenen.
Je schrapt strategieën die prima zijn. Een pessimistische afhandeling van 12.6% van de transacties kostte deze strategie 2.1 Sharpe-punten ten opzichte van 0.94 bij afhandeling op tickdata. Als het werkelijke getal 0.94 is en jouw conventie 0.18 rapporteert, gooi je het idee weg en ga je aan iets slechters werken. Voorzichtigheid die er 2 Sharpe-punten naast zit, is geen voorzichtigheid; het is ruis met een moreel sausje.
Erger nog: het verstoort de optimalisatie. Geef een parametersweep een pessimistische beslisregel en de optimizer leert onduidelijkheid te vermijden, omdat die nu een zuivere straf oplevert. De optimizer stuurt aan op ruime stops en nabije doelen, of op lange candles waarop de 2 niveaus zelden tegelijk voorkomen. Vervolgens presenteert die parameters die zijn gekozen op basis van je uitvoeringsconventie in plaats van de markt. Hetzelfde probleem als bij de gunstige versie, maar met het tegenovergestelde teken, en net zo onzichtbaar in de tearsheet.
Vuistregel die we altijd als eerste gebruiken: als stop_distance + target_distance kleiner is dan het bereik op het 75e percentiel van je candle-interval, weegt je intrabar-aanname zwaarder mee in de P&L dan je signaal. Bereken beide getallen. Dat kost 4 regels en deze controle heeft meer strategiereviews beëindigd dan welke andere controle ook.
Wat wel werkt
Het koersverloop binnen een candle is data. Haal die op, of baken af wat je niet kunt ophalen.
- Handel af op de fijnste reeks die je hebt. Binance aggTrades voor de relevante minuten bestaat uit een paar honderd rijen en geeft meteen uitsluitsel: welk niveau als eerste werd geraakt en tegen welke prijs de sweep werd uitgevoerd. Je hebt geen tickdata nodig voor de volledige backtest, alleen voor de onduidelijke candles. In mijn steekproef ging het om 606 minuten van de 129,600. Dat is een kleine download, geen infrastructuurproject.
- Zijn ticks niet beschikbaar, gebruik dan voor de afhandeling 1 of 2 lagere timeframes. Signalen op 15m, exits afgehandeld op candles van 1s of 1m. De onduidelijkheid daalt van 49% naar een fractie van een procent, en wat overblijft is klein genoeg om eerlijk te negeren.
- Rapporteer altijd de bandbreedte. Draai elke backtest 2 keer: met optimistische en pessimistische afhandeling, en zet beide Sharpes naast die van de afhandeling op basis van data. Dat verschil is je intrabar-onzekerheid. Het hoort op de tearsheet, naast het betrouwbaarheidsinterval dat je ook voor de Sharpe zelf zou vermelden. Als de bandbreedte 0.2–2.3 is, is geen enkele conclusie daarbinnen echt.
- Houd het percentage onduidelijke gevallen bij als een volwaardige maatstaf. Bij ons staat die bovenaan elke strategiekaart, naast het aantal transacties en de omzet. Een percentage boven ongeveer 5% betekent dat je de exitlogica test, niet de entrylogica.
- Modelleer de trigger los van de uitvoering. Activeer de trigger op de prijsreeks die het handelsplatform daadwerkelijk gebruikt; voer uit tegen de triggerprijs plus slippage die is getrokken uit een verdeling gekalibreerd op basis van de tickdata, niet tegen de triggerprijs zelf.
Bij aandelen is hetzelfde probleem er, maar in een andere gedaante. Een stop op 62.00 voor een aandeel dat 's nachts met een koersgat op 58.40 opent, wordt niet uitgevoerd op 62.00. De uitvoering ligt ergens onder de openingskoers. Een backtest op dagcandles die bij koersgaten −$0.00 slippage boekt, vertelt je vrolijk dat een stop-loss-overlay je drawdown heeft verbeterd. Dat is niet zo. De strategie is gewoon nooit getest op de dagen die ertoe doen. Handelsonderbrekingen hebben hetzelfde effect: je stop wordt pas uitgevoerd bij de heropeningsveiling, tegen een prijs die de low van de candle nooit laat zien.
Dit is allemaal niet ingewikkeld. Het komt erop neer dat een candle een samenvatting is, en dat een strategie met een stop en een doel een gok is op de volgorde van gebeurtenissen die uit die samenvatting is weggelaten. Wanneer de papertrading-engine de strategie eindelijk tegen live koersdata draait, heeft die data een eigen oordeel over die volgorde. De data heeft zich nog nooit iets aangetrokken van welke vertakking van het if-statement je als eerste typte.
← Alle artikelen


