← Terug naar artikelen
July 16, 2026
5 min leestijd

Fill-simulatie: de ladder van close-price-fantasie naar queue-aware realiteit

Fill-simulatie: de ladder van close-price-fantasie naar queue-aware realiteit
#fill simulatie
#backtest
#limit orders
#queue position
#partial fills
#market making
#executie
#market microstructure

Je backtest bevat twee modellen: een model van je alpha en een model van je fills. De meeste mensen steken 95% van hun moeite in het eerste en erven het tweede van welk framework ze toevallig gebruiken. Dat is achterstevoren. Een middelmatig signaal met een eerlijk fill-model levert een middelmatige maar reële PnL-schatting op. Een geweldig signaal met fill_price = candle.close levert een getal op dat nergens een schatting van is — het is de uitkomst van een aanname die je nooit hebt onderzocht.

Voor taker-strategieën op liquide instrumenten is het fill-model een correctieterm. Voor alles met resting limit orders — market making, passieve entries, post-only rebate harvesting — is het fill-model de strategie. Of je gevuld wordt, wanneer, in welke hoeveelheid, en afhankelijk van welke daaropvolgende prijsbeweging bepaalt het teken van de PnL, niet alleen de grootte ervan.

In Backtest-live parity hebben we de volledige taxonomie van backtest-live-divergenties in kaart gebracht en executiedivergentie beoordeeld als 5/5 severity — de zwaarste klasse. Dat artikel gaf fill-simulatie drie grove nauwkeurigheidsniveaus en ging verder. Dit is de diepgaande metgezel: de complete ladder, trede voor trede, met de order-lifecycle als state machine, fill-probability bounds die je daadwerkelijk kunt berekenen, en een gemeten experiment dat precies laat zien waar op de ladder een "winstgevende" maker-strategie sterft.

De ladder

Vijf-tredige ladder van fill-simulatiefideliteit

Elke trede vereist meer data en meer code, en elke trede verwijdert een specifieke systematische bias. De treden zijn geordend naar wat ze fout doen, niet alleen naar kosten.

Trede 0: close-price fill

fill_price = bar.close

De order wordt direct, volledig gevuld, tegen de close van de bar die het signaal genereerde. Het signaal is berekend op basis van diezelfde close, dus je handelt op data die nog niet bestond toen de prijs werd geprint: look-ahead bias in een executiekostuum. Elke strategie met turnover ziet er hier goed uit.

Trede 1: next-bar open

fill_price = next_bar.open

Het minimale eerlijke model voor taker-logica op bars. Het signaal wordt berekend op bar tt, de fill vindt plaats op de eerste observeerbare prijs van bar t+1t+1. Dit elimineert de look-ahead maar veronderstelt nog steeds nul spread, nul impact, oneindige liquiditeit bij de open-print en 100% fill-zekerheid. Voor limit orders degenereert het tot touch-fill-logica (hieronder meer over waarom dat vergif is).

Trede 2: spread + vaste slippage

half_spread = mid * spread_bps / 2e4
slip        = mid * slippage_bps / 1e4
fill_price  = mid + side * (half_spread + slip)   # side: +1 buy, -1 sell

Nu betaalt elke taker-trade de halve spread plus een gekalibreerde constante. Dit is de eerste trede waarop een high-turnover strategie in de backtest kan sterven — en dat is precies het punt. De resterende fout: slippage is niet constant. Het schaalt met de ordergrootte relatief tot de zichtbare diepte en explodeert precies wanneer je strategie het meest wil handelen. Een vaste 5 bps is een gemiddelde over regimes; je strategie handelt niet in het gemiddelde regime.

Trede 3: L2 depth-walk voor market orders

Met orderboek-snapshots stop je met slippage raden en ga je het berekenen. Een market buy van grootte QQ loopt de ask-kant af, niveau voor niveau; de fill-prijs is het volume-gewogen gemiddelde over de geconsumeerde niveaus:

Pfill(Q)=1Qipimin ⁣(qi,  Qj<iqj)P_{\text{fill}}(Q) = \frac{1}{Q}\sum_{i} p_i \cdot \min\!\left(q_i,\; Q - \textstyle\sum_{j<i} q_j\right)

def depth_walk(levels: list[tuple[float, float]], qty: float) -> tuple[float, float]:
    """levels: [(price, size), ...] sorted best-first. Returns (vwap, filled_qty)."""
    remaining, cost = qty, 0.0
    for price, size in levels:
        take = min(size, remaining)
        cost += take * price
        remaining -= take
        if remaining <= 0:
            break
    filled = qty - remaining
    return (cost / filled if filled > 0 else float("nan"), filled)

Twee correcties maken dit wezenlijk eerlijker. Ten eerste, latency: loop het boek af zoals het eruitzag Δt\Delta t na je beslissingstijdstip, waarbij Δt\Delta t je gemeten signal-to-exchange latency is — het boek dat je zag is niet het boek dat je raakte. Ten tweede, partial marketable fills: als de zichtbare diepte binnen je limit-through-prijs kleiner is dan QQ, moet het model een partial fill teruggeven en een resterend deel achterlaten dat de order op de boek zet, wat het probleem doorgeeft aan de state machine hieronder.

De resterende fout op trede 3 is impact en refill: je consumeert het boek als een statisch object, maar echte boeken vullen zich gedeeltelijk aan (en echte tegenpartijen reageren). Voor cliporders onder een paar procent van de top-of-book-diepte is deze fout klein; voor grotere clips heb je een impactmodel (Almgren-Chriss 2001) nodig bovenop dit alles.

Trede 4: probabilistische queue-position fills voor limit orders

Treden 0-3 beantwoorden "tegen welke prijs vult mijn agressieve order." Trede 4 beantwoordt de moeilijkere vraag: vult mijn passieve order überhaupt — en het is de enige trede die een maker-strategie kan prijzen. Een resting limit order op prijs pp wordt gevuld wanneer het cumulatief verhandelde volume op pp het queue-volume overschrijdt dat ervoor lag. Dat vereist het volgen van je positie in een FIFO-queue die je niet direct kunt observeren.

De machinerie voor queue-position-schatting — beginpositie, update-regels bij trades versus cancels, de f(x)f(x)-kansfamilie voor het toewijzen van niet-geobserveerde cancellations — is de primitief die we hebben gebouwd in Queue inside the wall. Ik zal die hier niet opnieuw afleiden; de simulator consumeert hem. Wat trede 4 toevoegt is de fill-beslisregel bovenop de schatting, behandeld in de sectie over fill-probability hieronder.

Waarom deze trede ertoe doet, wordt gekwantificeerd in de literatuur: Moallemi en Yuan (2016), "A Model for Queue Position Valuation in a Limit Order Book," laten zien dat voor large-tick-instrumenten de economische waarde van een front-of-queue-positie versus back-of-queue vergelijkbaar is met de half-spread — dezelfde ordegrootte als de gehele theoretische edge van een market-making-strategie. Een fill-model dat queue-positie negeert onderschat een maker's PnL niet — het schat de PnL van een andere strategie.

Er is een trede 5 — volledige agent-based simulatie waarbij de markt reageert op je orders (queue-reactive modellen in de zin van Huang, Lehalle en Rosenbaum 2015; multi-agent frameworks zoals ABIDES, Byrd et al. 2020). Historische replay, zelfs queue-aware, veronderstelt dat je order niets verandert aan het gedrag van iedereen anders. Die aanname klopt aardig bij retailgrootte en wordt steeds problematischer naarmate je quotes een zichtbaar deel van het niveau uitmaken. Trede 5 valt hier buiten scope; weet dat de ladder niet bij 4 eindigt.

Partial fills als state machine

Treden 0-2 kunnen doen alsof een order een functieaanroep is: indienen, prijs krijgen, klaar. Vanaf trede 3 is een order een proces met een lifecycle, en de simulator moet dit als state machine modelleren of hij zal precies de gevallen die er het meest toe doen stilzwijgend verkeerd afhandelen.

Order-lifecycle state machine met queue-position-annotaties

enum OrderState {
    PendingNew,                                  // sent, not yet acked (latency window)
    Resting     { remaining: f64, q_ahead: f64 }, // in book, queue position estimated
    PartialFill { remaining: f64, q_ahead: f64 }, // some qty done, rest still queued
    PendingAmend,                                 // amend in flight
    PendingCancel,                                // cancel in flight
    Filled,
    Canceled    { filled_qty: f64 },              // may be partially filled at cancel time
    Rejected,
}

De transities dragen de economie:

  • PendingNewResting: de order voegt zich bij de queue achter alles wat aanwezig was op ack-tijdstip, niet op beslissingstijdstip. Je queue-positie wordt geseed met het niveauvolume zoals het was op t+Δtackt + \Delta t_{\text{ack}}. Simulatoren die seeden op beslissingstijdstip overschatten systematisch de queue-prioriteit — met precies het volume dat binnenkwam tijdens je latency-window, wat het meeste volume is tijdens bursts.
  • RestingPartialFill: een trade op jouw niveau die groter is dan de queue ervoor vult je gedeeltelijk. Het restant behoudt zijn (nu front-of-queue) positie. Partial fills zijn geen ruis — het is informatie: gevuld worden voor 0,3 van 1,0 en zien hoe de prijs wegstuitert is een ander PnL-event dan een volledige fill, en het inventarisproces van een maker is opgebouwd uit deze fragmenten.
  • RestingPendingAmendResting: de valstrik. Op vrijwel elk crypto-venue is een amend een cancel/replace — Binance's cancelReplace is atomisch tegen dubbele executie, maar geeft een nieuw order-ID achteraan de queue terug. Zelfs venues met native modify-semantiek (CME Globex) behouden tijdprioriteit alleen bij een quantity decrease; een prijswijziging of quantity-increase verspeelt die. Dus in de simulator: elke prijsamend reset q_ahead naar het volledige huidige niveauvolume. Een quoting-engine die elke 500ms opnieuw pegt "onderhoudt" geen quote — hij treedt voortdurend opnieuw toe achteraan de queue, en zijn realistische fill-profiel is bijna pure adverse selection. Gecombineerd met het Moallemi-Yuan-resultaat: her-quoten heeft een prijs, en die prijs is je queue-positie.
  • PendingCancelPartialFillCanceled: cancels kosten ook latency. In het venster tussen het besluit om een quote terug te trekken en het bereiken van de matching engine door de cancel, kun je nog steeds gevuld worden — en die fills zijn de slechtste fills die je ooit zult ontvangen, omdat de reden dat je de quote terugtrok was dat de markt op het punt stond je te overrijden. Een simulator zonder cancel-latency verwijdert precies de meest toxische fills uit je geschiedenis.

De state machine is ook wat de boekhouding van de simulator eerlijk maakt: fees lopen op per fill-event, inventaris update per fill-event, en time-in-state-statistieken (hoe lang orders resten voordat ze gevuld worden versus gecanceld) worden direct vergelijkbaar met live logs — waar de calibratielus aan het einde op teert.

Limit-fill probability: drie modellen en een bracket

Gegeven een order die rust op prijs pp (zeg een bid), wanneer verklaart de simulator een fill? Drie beslisregels, in oplopende eerlijkheid:

1. Touch-fill (naïef). Fill als de prijs je niveau raakt: lowtp\text{low}_t \le p. Dit is een first-passage-time-regel, en het falen ervan is een kwart eeuw geleden al gemeten: Lo, MacKinlay en Zhang (2002), "Econometric Models of Limit-Order Executions" (Journal of Financial Economics 65), fitten survival-modellen op werkelijke limit-order-data en concluderen dat hypothetische executies geconstrueerd uit first-passage-times "zeer slechte proxy's zijn voor werkelijke limit-order-executies." De faalmodus is structureel: wanneer de prijs je niveau raakt en terugstuitert, heeft de touch de voorkant van de queue geconsumeerd — de traders die vóór jou quoteerden. Touch-fill kent jou hun fills toe. Erger nog, het kent precies de goede fills toe (touch-and-bounce is het winstgevende scenario voor een maker), terwijl je echte fill-set scheef getrokken is naar touch-and-run-through — de adversely selected fills.

2. Trade-through (conservatieve bound). Fill alleen als de prijs strikt door je niveau handelt: lowt<pϵ\text{low}_t < p - \epsilon, of bij tick-data, cumulatief verhandeld volume op pp overschrijdt het gehele niveau. Als de prijs erdoorheen handelde, is de hele queue op pp geconsumeerd, dus jij bent gevuld ongeacht positie. Dit kent nooit een fill toe die je niet zou hebben gekregen. De bias is het spiegelbeeld van die van touch-fill: het ontzegt je elke fill waarbij de queue uitgeput raakte tot jouw positie zonder volledige penetratie, en de fills die het wél toekent zijn onevenredig de run-through-fills (adversely selected). Een maker-backtest onder trade-through is een stresstest, geen schatting.

3. Queue-depletion-schatting. Volg cumulatief verhandeld volume MtM_t op jouw prijs vanuit de trade tape en gecancelde volume CtC_t afgeleid uit L2-deltas. Je geschatte queue ervoor:

Q^ahead(t)=max ⁣(Q0MtϕCt,  0)\hat{Q}_{\text{ahead}}(t) = \max\!\Big(Q_0 - M_t - \phi \cdot C_t,\; 0\Big)

waarbij Q0Q_0 het niveauvolume is bij (ack-tijdstip) orderinvoer en ϕ[0,1]\phi \in [0,1] de fractie cancellations is die verondersteld wordt vóór jou te komen — de knop waarvan de principiële vorm (de f(x)f(x)-familie) is afgeleid in het queue-position-artikel. Fill begint wanneer Q^ahead=0\hat{Q}_{\text{ahead}} = 0; je gevulde hoeveelheid is het verhandelde volume boven op dat punt, wat partial fills op natuurlijke wijze oplevert:

filled(t)=min ⁣(S,  max(MtQ0+ϕCt,0))\text{filled}(t) = \min\!\Big(S,\; \max(M_t - Q_0 + \phi\, C_t,\, 0)\Big)

Het instellen van ϕ=1\phi = 1 (alle cancels vóór jou) geeft de optimistische rand van dit model; ϕ=0\phi = 0 de pessimistische rand. Wanneer je alleen schaarse L2-snapshots hebt en geen trade tape op het niveau — gebruikelijk bij 100ms-getroffelde crypto-feeds — kun je terugvallen op een modelgebaseerde prior: Cont, Stoikov en Talreja (2010), "A Stochastic Model for Order Book Dynamics" (Operations Research 58), modelleren elk prijsniveau als een birth-death-queue en berekenen, via Laplace-transformaties, de kans dat een order op de bid uitgevoerd wordt voordat de mid beweegt, gegeven de huidige queue-groottes. Het is een analytisch fill-probability-orakel: ruw in vergelijking met tape-replay, veel beter dan touch-fill, en goedkoop genoeg om te evalueren binnen een hete backtest-lus.

De bracket-discipline

De drie regels zijn geen concurrenten — ze zijn een ordening:

fillstrade-through    fillsqueue    fillstouch\text{fills}_{\text{trade-through}} \;\subseteq\; \text{fills}_{\text{queue}} \;\subseteq\; \text{fills}_{\text{touch}}

wat een PnL-bracket induceert. Draai elke maker-backtest drie keer en rapporteer het interval:

PnLtrade-through    PnLlivePnLtouch\text{PnL}_{\text{trade-through}} \;\le\; \text{PnL}_{\text{live}} \lesssim \text{PnL}_{\text{touch}}

(de bovenste ongelijkheid is bij benadering — touch-fill kan strategieën verkeerd rangschikken, niet alleen opblazen, omdat het je contrafeitelijke goede fills toekent). De beslisregel die hieruit volgt: een maker-strategie is alleen inzetbaar als hij overleeft aan de conservatieve rand van de bracket, en de bracket smal genoeg is dat de puntschatting iets betekent. Een strategie die +9ktouch/9k touch / -3k trade-through laat zien heeft een 12kbredebracket;jeweetnietsbehalvedatdeaannamevanjesimulatorjealphadomineert.Eenstrategiedie+12k-brede bracket; je weet niets behalve dat de aanname van je simulator je alpha domineert. Een strategie die +2,1k / +$0,4k laat zien vertelt je iets reëels.

Het experiment: één maker-strategie de ladder af

Maker-strategie-PnL die trede voor trede instort langs de fill-model-ladder

Neem één bewust eenvoudige maker — symmetrische quotes op best bid/ask, vaste clips van 0,05 BTC, inventaris begrensd op ±0,5 BTC met taker-flattening bij de cap. Eén maand BTCUSDT-perpetual-data: 1m bars voor de lage treden, 100ms L2-diffs plus de trade tape voor de hoge treden; maker fee 1,0 bps, taker 4,0 bps. Dezelfde signaalcode op elke trede (gedeelde kern, zodat de enige variabele het fill-model is). De onderstaande cijfers zijn één representatieve run van ons — jouw magnitudes zullen verschillen per venue, maand en grootte; de vorm niet:

Trede Fill-model Quote-fillrate Fills Maand-PnL Oordeel
0 close-fill 98% 41.200 +$14.800 fantasie
1 next-bar-open / touch op 1m bars 89% 37.400 +$9.600 fantasie met latency
2 touch + spread & vaste slippage op taker-flattens 89% 37.400 +$7.100 kosten gemodelleerd, fills nog steeds fictief
3 + L2 depth-walk op taker-flattens 89% 37.400 +$6.400 exits eerlijk, entries nog steeds fictief
4a trade-through (conservatief) 21% 8.900 -$3.900 stress bound
4b queue-depletion, ϕ\phi gekalibreerd 37% 15.600 -$700 beste schatting
4c queue-depletion, ϕ=1\phi = 1 (optimistisch) 44% 18.700 +$1.900 bovenrand
live shadow run, dezelfde maand 35% 14.100 -$1.150 realiteit

Lees de tabel van boven naar beneden en kijk waar de strategie sterft. Het is niet trede 2 — fees en slippage schaven er 26% af en de strategie ziet er nog steeds robuust winstgevend uit. Hij sterft tussen trede 3 en trede 4, en hij sterft om een reden die geen kostenmodel kan vangen: fill-selectie. Touch-fill kende 37.400 fills toe waarvan de meerderheid touch-and-bounce was — pure spread-capture. Het queue-aware model verwijderde 58% van die fills, en de fills die het verwijderde waren onevenredig de winstgevende: wanneer het niveau licht wordt aangetikt, absorbeert de queue vóór een her-quotende retail-latency-maker alles. De fills die trede 4 overleven zijn scheef getrokken naar level-clearing sweeps — de fills waarbij de prijs al door je heen beweegt. De fillrate daalde 2,4x; de PnL wisselde van teken. Die asymmetrie — de goede fills verliezen, de slechte behouden — is adverse selection mechanisch expliciet gemaakt, en het is onzichtbaar op elke trede onder 4.

Let ook op wat de bracket deed: [-3.900,+3.900, +1.900] omvat nul, met de gekalibreerde schatting op -700enliveop700 en live op -1.150. De simulator raakte live PnL niet exact op de dollar — hij kreeg het teken, de magnitude, en de fillrate binnen 2 punten. Daar is een fill-model voor. De rung-1-backtest miste live PnL met $10.750 op een strategie waarvan de gehele maandelijkse bruto-edge een paar duizend dollar was: de fill-modelfout was ongeveer 3x zo groot als de alpha. Vandaar de these: je fill-model is een grotere aanname dan je alpha.

Eén voorbehoud voor de lage treden: als je op bar-data moet leven (treden 0-2), los dan minstens intrabar-ambiguïteit op met adaptive drill-down — van 1m naar 1s/100ms/trades boren waar SL, TP of quote-niveaus binnen het bar-bereik vallen. Drill-down fixt sequencing-fouten (welk niveau werd eerst geraakt) maar kan geen queue-fouten fixen; het is de data-resolutie-metgezel van dit artikel, geen vervanging voor trede 4.

De calibratielus: sluiten tegen live fills

Een trede-4-simulator heeft vrije parameters — ϕ\phi, latency Δt\Delta t, cancel-latency, de niveau-refill-aanname. Ongekalibreerd is het slechts een anders gevormde gok. De lus die er een instrument van maakt:

1. Log alles live. Elk order-event met exchange-tijdstempels: indienen, ack, elke partial fill, amend-acks, cancel-acks. Plus de L2-state op indientijdstip. Dit is dezelfde loggingdiscipline die backtest-live parity vereist voor zijn DivergenceMonitor — de fill-modelcalibratie is de diepste laag van die monitor.

2. Replay dezelfde orders door de simulator. Voer de opgenomen marktdata en de opgenomen order-instructies (niet de fills) in de simulator in. Nu heb je gepaarde uitkomsten: voor elke live order, een gesimuleerd lot.

3. Vergelijk verdelingen, niet gemiddelden, per bucket. Eén enkele globale fillrate-match kan compenserende fouten verhullen (te optimistisch in rustige regimes, te pessimistisch tijdens bursts — nettend naar "gekalibreerd"). Bucket op de drivers:

import numpy as np
from scipy.stats import ks_2samp

def calibration_report(pairs, bucket_key):
    """pairs: [{'bucket':…, 'live_filled':bool, 'sim_filled':bool,
                'live_ttf':float|None, 'sim_ttf':float|None}, …]"""
    out = {}
    for b in sorted({p['bucket'] for p in pairs}):
        grp = [p for p in pairs if p['bucket'] == b]
        live_fr = np.mean([p['live_filled'] for p in grp])
        sim_fr  = np.mean([p['sim_filled']  for p in grp])
        live_ttf = [p['live_ttf'] for p in grp if p['live_ttf'] is not None]
        sim_ttf  = [p['sim_ttf']  for p in grp if p['sim_ttf']  is not None]
        ks = ks_2samp(live_ttf, sim_ttf) if len(live_ttf) > 20 and len(sim_ttf) > 20 else None
        out[b] = {
            'n': len(grp),
            'fill_rate_live': live_fr,
            'fill_rate_sim':  sim_fr,
            'fill_rate_gap':  sim_fr - live_fr,      # signed: + means sim optimistic
            'ttf_ks_pvalue':  ks.pvalue if ks else None,
        }
    return out

Twee statistieken per bucket: de signed fill-rate gap (simulator minus live) en een KS-test op time-to-fill-verdelingen onder gevulde orders. De time-to-fill-vergelijking is de scherpe — een simulator kan fillrates matchen terwijl hij systematisch op verkeerde tijdstippen vult, wat elke downstream inventaris- en adverse-selection-statistiek corrumpeert. Dit is precies de les van Lo-MacKinlay-Zhangs survival-analysis-framing: executie is een time-to-event-probleem, dus valideer het als zodanig.

4. Fit de knoppen, in volgorde van identificeerbaarheid. Latency eerst (direct gemeten uit ack-tijdstempels — niet gefit). Dan ϕ\phi door de fill-rate-gap over queue-depth-buckets te minimaliseren. Controleer daarna de vol-regime-buckets: een aanhoudende optimistische gap geconcentreerd in burst-buckets betekent meestal dat je simulator cancel-latency-toxische fills of niveau-refill ondermodelleerd, niet ϕ\phi.

5. Draai de bracket opnieuw. Na calibratie moet de queue-depletion-schatting binnen de bracket vallen, dicht bij live, en — de echte acceptatietest — de rangschikking van strategievarianten onder de simulator moet overeenkomen met hun rangschikking in shadow mode. Bevries dan de parameters en herkalibreer op een schema; fill-dynamiek drift met de fee-tiers van het venue, tick-size-wijzigingen, en de HFT-populatie, en een ϕ\phi gekalibreerd in maart is een hypothese in juli.

Convergentieverwachtingen uit onze runs: een ongekalibreerde trede-4-simulator komt doorgaans binnen ±10-15 punten van de live fillrate uit; na één calibratiepas, ±3-5 punten, met time-to-fill-KS-p-waarden die niet langer uniform verworpen worden. Je zult het met historische replay niet beter doen dan dat — het residu is de reactie van de markt op jou, wat het probleem van trede 5 is.

Wat mee te nemen

  1. Noem je trede. Elke backtest staat op deze ladder, of je de trede nu koos of je framework hem voor je koos. Als je het fill-model van je trede en de bekende biases ervan niet kunt benoemen, heeft je PnL-getal foutmarges die je nog niet hebt gezien.
  2. Takers kunnen stoppen bij trede 3. Depth-walk plus gemeten latency prijst agressieve executie eerlijk op retailgrootte. Besteed de bespaarde moeite aan datakwaliteit.
  3. Makers beginnen bij trede 4. Daaronder is limit-order-fill-logica niet bij benadering fout — het selecteert een contrafeitelijke fill-set met tegengestelde adverse-selection-skew. Touch-fill-backtests van maker-strategieën zijn de meest betrouwbare generator van strategieën die sterven bij contact met productie.
  4. Modelleer de lifecycle, niet de fill. Partial fills, amend-reset-queue, cancel-latency — de state machine is waar de toxische fills leven, en toxische fills zijn waar maker-PnL sterft.
  5. Rapporteer de bracket. Conservatieve en optimistische bounds kosten twee extra backtest-runs en zetten "mijn backtest zegt +7k"omin"derealiteitligtergensin[7k" om in "de realiteit ligt ergens in [-3,9k, +$1,9k]" — wat een andere, en betere, beslissing is.
  6. Kalibreer tegen live fills in buckets. Een fill-model dat alleen in aggregaat gevalideerd is, is een fill-model met verborgen compenserende fouten. Bucketed fill-rate gaps plus time-to-fill-KS-tests, elk kwartaal herkalibreren.

De treden van de ladder zijn geen academische gradaties — elke trede is een specifieke leugen die je backtest je niet langer vertelt. Klim tot de leugens kleiner zijn dan je edge.

Nuttige links

  1. Moallemi, C., Yuan, K. — A Model for Queue Position Valuation in a Limit Order Book (2016)
  2. Cont, R., Stoikov, S., Talreja, R. — A Stochastic Model for Order Book Dynamics, Operations Research 58(3), 549-563 (2010)
  3. Lo, A., MacKinlay, C., Zhang, J. — Econometric Models of Limit-Order Executions, Journal of Financial Economics 65(1), 31-71 (2002)
  4. Huang, W., Lehalle, C.-A., Rosenbaum, M. — Simulating and Analyzing Order Book Data: The Queue-Reactive Model, JASA 110(509), 107-122 (2015)
  5. Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
  6. Byrd, D., Hybinette, M., Balch, T. — ABIDES: Towards High-Fidelity Multi-Agent Market Simulation (2020)
  7. Binance API — Cancel-Replace order semantics
  8. CME Globex — Order modification and time priority rules

Citation

@article{soloviov2026fillsimulation,
  author = {Soloviov, Eugen},
  title = {Fill simulation: the ladder from close-price fantasy to queue-aware reality},
  year = {2026},
  url = {https://marketmaker.cc/blog/fill-simulation-partial-fills-backtest},
  description = {Five rungs of fill simulation fidelity — from close-price fills to probabilistic queue-position models. Partial fills as a state machine, limit-fill probability bounds as a PnL bracket, and a calibration loop against live fills.}
}
Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.