2 september 2026 · microstructure

Je limietorder werd niet gevuld: een brief over queuepositie, maker-backtests en de Sharpe die je zelf hebt verzonnen

Je limietorder werd niet gevuld: een brief over queuepositie, maker-backtests en de Sharpe die je zelf hebt verzonnen

Je stuurde me vorige week het notebook: hetzelfde signaal, hetzelfde universum, dezelfde 14 maanden BTCUSDT-perpdata. Alleen de uitvoering veranderde. Je stopte met het kruisen van de spread en begon biedorders één tick binnen de spread te laten rusten. De Sharpe steeg van 0.42 naar 2.14. Je vroeg of dat echt was, of dat je iets kapot had gemaakt.

Je hebt iets kapotgemaakt. Ik wil precies laten zien waar, want de bug is één regel en de les erachter is veel groter dan die regel.

Dit is je fillregel, rechtstreeks uit je engine gekopieerd:

if bar.low <= limit_price: fill(limit_price)

Dat betekent: als de markt tijdens deze minuut een trade op of onder mijn prijs noteerde, ben ik gevuld op mijn prijs. Wat er in werkelijkheid staat, is dat de prijs je niveau raakte. Een niveau raken is niet hetzelfde als een fill krijgen. Daartussen zit een queue, en jij hebt die queue gemodelleerd alsof hij leeg is.

Wat er vóór je staat

Wanneer je op BTCUSDT perp een biedorder plaatst op 84,120.0, sluit je achteraan aan bij alles wat daar al rust. Op het beste biedniveau van dat contract staat doorgaans ergens tussen 4 en 30 BTC, afhankelijk van het tijdstip; de mediaan in je steekproefperiode ligt rond 12 BTC. Je order is 0.4 BTC. Om een trade te krijgen, moeten verkopers dat niveau met genoeg totale omvang raken om iedereen weg te werken die vóór jou aankwam, en dat moet gebeuren voordat het niveau onder je wordt geannuleerd of de markt omhoog beweegt.

Je backtest zou dus niet moeten vragen: ‘heeft de prijs 84,120.0 bereikt?’, maar: ‘zijn er, terwijl mijn order daar rustte, minstens 12.4 BTC aan marktverkopen uitgevoerd op 84,120.0?’ Dat zijn totaal verschillende gebeurtenissen. In je data komen ze ongeveer een factor drie minder vaak voor.

12.4 BTCmediane omvang die vóór je staat wanneer de prijs je limiet raakt
100%fillrate die je backtest aanneemt
31%van de aanrakingen waarbij je queue werd weggewerkt
4,180trades, waarvan er ongeveer 1,300 overblijven

Drie fillregels, drie verschillende strategieën

Ik heb je signaal opnieuw doorgerekend met dezelfde entries en drie uitvoeringsmodellen. Dezelfde alpha, dezelfde kosten, dezelfde funding. Alleen de filllogica veranderde.

FillregelFillsGemiddelde edge 60s na fillSharpe
Raak: low <= limit4,180+2.6 bp2.14
Strikte doorbraak: low < limit - 1 tick1,712+0.4 bp0.61
Queue-simulatie op volume per prijsniveau1,306+0.9 bp0.77

De middelste rij is de snelle oplossing waar iedereen als eerste naar grijpt: tel een fill alleen mee als de markt strikt door je prijs heen handelt, vanuit de gedachte dat de markt je order moet hebben geconsumeerd als de prijs verderging. Dat klopt ongeveer en haalt het grootste deel van de fictie weg. Het is ook op een vervelende manier vertekend; daar kom ik zo op terug.

De onderste rij is het model dat je moet bouwen. Je hebt er geen L3-feed en geen reconstructie per order voor nodig. Je hebt het meeste al.

Een queue-simulatie die je met aggTrades kunt bouwen

Haal de geaggregeerde tradestroom op in plaats van klines. Elke print bevat een prijs, hoeveelheid, tijdstempel en maker-side-vlag, die aangeeft of de agressor kocht of verkocht. Dat is genoeg voor een bruikbare simulatie van je eigen passieve order:

  1. Maak een momentopname van de rustende omvang op jouw prijs zodra je order wordt geplaatst. Heb je alleen een orderboekfeed van 100ms, gebruik dan de laatste momentopname; die fout is klein vergeleken met de fout die je probeert te verhelpen.
  2. Stel queue_ahead = resting_size in. Wees pessimistisch en ga ervan uit dat je achteraan staat. Dat is zo, tenzij jij degene bent die het niveau creëert.
  3. Doorloop de tradestroom. Elke print met een verkopende agressor op of onder jouw prijs verlaagt queue_ahead met de hoeveelheid van die print. Zodra de waarde negatief wordt, ben je gevuld op jouw prijs en op dat tijdstip.
  4. Verplaats de queue niet terug naar nul als de prijs één tick van je af beweegt. Laat hem afnemen. Sommige orders vóór je worden geannuleerd wanneer het niveau niet langer actueel is, andere niet. Een afname van 30-40% per volledige seconde buiten het beste niveau sloot in mijn reconstructies beter aan dan beide uitersten.
  5. Als je strategie de order zou annuleren en opnieuw plaatsen, modelleer die nieuwe order dan als een order achteraan in de nieuwe queue. Deze stap slaan mensen over, en daar zit de resterende fictie.

Bij stap 2 wil je vast met me in discussie. Ja, soms sta je vooraan omdat je de order plaatste zodra het niveau ontstond. Prima: meet het, neem het niet zomaar aan. Log de rustende omvang op het moment van plaatsen en laat de data uitwijzen welk deel van je orders echt vroeg is. Bij jouw strategie was dat 11%, omdat je signaal na een koersbeweging afgaat. Het niveau waar je bij aansluit, bestaat dan al en er staat al een groep orders.

Je krijgt juist de fills die je liever niet had gehad

Nu komt het deel dat er echt toe doet, en de reden waarom de regel met strikte doorbraak vertekend is.

Denk na over wanneer je biedniveau volledig wordt weggekocht. Dat gebeurt wanneer de verkoopdruk groot genoeg is om het hele niveau op te eten. Dat is per definitie het moment waarop de markt omlaag door jouw prijs beweegt. De fills waar je het zekerst van bent, zijn de fills waarop je meteen aan de verkeerde kant van de markt zit.

Splits je fills uit naar hoe ze tot stand kwamen en meet de mark-out na 60 seconden:

FilltypeAandeel fillsMark-out na 60s
Op niveau gehandeld, prijs veerde op38%+3.1 bp
Op niveau gehandeld, prijs bleef vlak21%+0.2 bp
Prijs ging er met 2+ ticks doorheen41%−2.4 bp

Je naïeve backtest gaf je alle drie de groepen en rekende ze allemaal alsof ze gratis waren. De regel met strikte doorbraak levert bijna alleen de derde groep op. Daarom zakte de edge daar verder in dan bij de queue-simulatie. Geen van beide is juist. De queue-simulatie geeft je een realistische mix, en die mix is het hele spel: passieve uitvoering levert je de spread op en kost je geld door adverse selection. De verhouding tussen die twee is je werkelijke strategie.

De oude formulering van de aandelenhandeldesk klopt nog steeds: een maker-order is een gratis optie die je aan de markt hebt geschreven. Iemand oefent die uit wanneer dat voordelig is. Je backtest incasseerde de premie en vergat dat de optie ook een uitbetaling kan opleveren.

De trades die je misliep veranderen de strategie, niet alleen de kosten

Dit is het punt waar ik het liefst bij wil stilstaan. Als je taker-uitvoering slecht modelleert, krijg je de juiste trades tegen de verkeerde prijs, en met een correctie voor de fees los je het grootste deel op. Als je maker-uitvoering slecht modelleert, krijg je de helemaal verkeerde set trades. Ongeveer 2,900 van je 4,180 entries hebben nooit plaatsgevonden. Een deel daarvan waren je beste signalen, op candles die omhoog piekten en weer omkeerden. Precies in zulke situaties ging de markt zonder jou ervandoor.

Je tak voor niet-gevulde orders heeft dus echte logica nodig. Wat doet de strategie als de entry niet is gevuld tegen de tijd dat het signaal verouderd is? De markt achterna met een taker-order, en de spread plus impact betalen? Lager opnieuw plaatsen en een andere entrybasis accepteren? De trade overslaan en aan de zijlijn blijven? Elke keuze levert een wezenlijk andere equitycurve op, en geen daarvan is ‘doe alsof je gevuld was’. In onze runs haalde een eerlijke chase-regel, waarbij we na 20 seconden zonder fill de spread kruisten met een slippage-limiet van 3 bp, ongeveer een derde van de gemiste trades terug en ongeveer de helft van het verschil in Sharpe tussen de naïeve aanpak en de queue-simulatie. Dat is een opvallend interessant resultaat, dat pas zichtbaar wordt zodra het fillmodel goed genoeg is om de vraag zinvol te maken.

Snelle sanitycheck, tien minuten: neem je live papertradinglog en je backtest over dezelfde periode. Vergelijk de fillrate, niet de PnL. Als de backtest 100% van de rustende orders vult en de papertrading 34%, kun je je strategieën niet vergelijken: er zit een bug in je fillmodel. Los die op voordat je ook maar naar één rendementscijfer kijkt.

Nog twee kleinere punten nu ik je toch spreek

Post-only-afwijzingen. Gebruik je post-only om het maker-feetarief te garanderen en beweegt het orderboek tussen jouw beslissing en de bevestiging van de exchange, dan wordt de order afgewezen in plaats van geplaatst. In onze paperlogs gebeurt dat bij 3-6% van de pogingen op BTCUSDT tijdens normale handelsuren en bij meer dan 15% in de minuut na een Amerikaanse CPI-publicatie. Een afgewezen order is geen gevulde order en ook geen rustende, niet-gevulde order: het is een trade die nooit heeft bestaan. Als die status ontbreekt in je backtest, is je aantal trades juist opgeblazen in de regimes die je het belangrijkst vindt.

Preventie van zelfhandel en je eigen marktimpact. Met 0.4 BTC beweeg je BTCUSDT niet, dus sla dit over. Maar je zei dat je dit op een alt-perp met middelgrote marktkapitalisatie wilt draaien, waar de nominale waarde aan de beste bied- en laatprijs vaak onder $15k ligt. Daar is jouw order wel een betekenisvol deel van de queue, en de hoeveelheid die je vóór je meetelt, omvat ook het effect van je vorige order. Zodra je omvang meer dan ongeveer 10% van het rustende niveau is, moet de simulatie rekening houden met het feit dat andere deelnemers op je reageren. Eerlijk gezegd zou ik op dat punt meer vertrouwen hebben in papertrading dan in welke simulatie dan ook die jij of ik kunnen schrijven.

Draai het opnieuw met de queue-simulatie en stuur me de mark-outtabel, uitgesplitst naar filltype. Als de groep met prijsbounce nog steeds het grootste deel van de edge oplevert en de groep waarbij de prijs erdoorheen ging niet alles opslokt, heb je misschien iets dat het waard is om op papier te zetten. Was het hele resultaat gebaseerd op de 2,900 fills die je nooit zou hebben gekregen, dan kun je dat beter nu ontdekken dan na vier weken kijken hoe een paperaccount niet doet wat het notebook beloofde.

limietordersqueuepositieadverse selectionbacktestencrypto-futures
← Alle artikelen