Simulation des fills : l'échelle de la fantaisie au prix de clôture jusqu'à la réalité tenant compte de la file d'attente
Votre backtest contient deux modèles : un modèle de votre alpha et un modèle de vos fills. La plupart des gens consacrent 95% de leurs efforts au premier et héritent du second de n'importe quel framework qu'ils utilisent. C'est à l'envers. Un signal médiocre avec un modèle de fill honnête produit une estimation de PnL médiocre mais réelle. Un excellent signal avec fill_price = candle.close produit un chiffre qui n'est l'estimation de rien — c'est le résultat d'une hypothèse que vous n'avez jamais examinée.
Pour les stratégies taker sur instruments liquides, le modèle de fill est un terme correctif. Pour tout ce qui implique des ordres limites en attente — market making, entrées passives, capture de rebate post-only — le modèle de fill est la stratégie. Que vous soyez fillé ou non, quand, en quelle quantité, et conditionnellement à quel mouvement de prix subséquent, cela détermine le signe du PnL, pas seulement sa magnitude.
Dans Backtest-live parity nous avons cartographié la taxonomie complète des divergences backtest-live et attribué à la divergence d'exécution une sévérité de 5/5 — la pire classe. Cet article donnait trois niveaux de précision grossiers pour la simulation des fills et passait à autre chose. Celui-ci en est le complément approfondi : l'échelle complète, échelon par échelon, avec le cycle de vie de l'ordre comme machine à états, des bornes de probabilité de fill que vous pouvez réellement calculer, et une expérience mesurée montrant exactement à quel échelon de l'échelle une stratégie maker « profitable » meurt.
L'échelle

Chaque échelon exige plus de données et plus de code, et chaque échelon élimine un biais systématique spécifique. Les échelons sont ordonnés selon ce qu'ils font mal, pas simplement selon leur coût.
Échelon 0 : fill au prix de clôture
fill_price = bar.close
L'ordre est fillé instantanément, entièrement, à la clôture de la bougie qui a généré le signal. Le signal a été calculé à partir de cette même clôture, donc vous tradez sur des données qui n'existaient pas au moment où le prix a été imprimé : un biais de look-ahead déguisé en costume d'exécution. N'importe quelle stratégie à turnover élevé a l'air bonne ici.
Échelon 1 : ouverture de la bougie suivante
fill_price = next_bar.open
Le modèle honnête minimal pour la logique taker sur bougies. Le signal est calculé sur la bougie , le fill se produit au premier prix observable de la bougie . Cela élimine le look-ahead mais suppose toujours un spread nul, un impact nul, une liquidité infinie au prix d'ouverture, et une certitude de fill à 100%. Pour les ordres limites, cela dégénère en logique de fill au contact (touch-fill) — on verra plus bas pourquoi c'est du poison.
Échelon 2 : spread + slippage fixe
half_spread = mid * spread_bps / 2e4
slip = mid * slippage_bps / 1e4
fill_price = mid + side * (half_spread + slip) # side: +1 achat, -1 vente
Désormais chaque trade taker paie la moitié du spread plus une constante calibrée. C'est le premier échelon où une stratégie à turnover élevé peut mourir en backtest — c'est le but. L'erreur résiduelle : le slippage n'est pas constant. Il évolue avec la taille de l'ordre par rapport à la profondeur affichée et explose précisément quand votre stratégie veut le plus trader. Un 5 bps fixe est une moyenne sur les régimes ; votre stratégie ne trade pas le régime moyen.
Échelon 3 : parcours de la profondeur L2 pour les ordres au marché
Avec les snapshots de carnet d'ordres, vous cessez de deviner le slippage et le calculez. Un achat au marché de taille parcourt le côté ask niveau par niveau ; le prix de fill est la moyenne pondérée par le volume à travers les niveaux consommés :
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)
Deux corrections rendent cela nettement plus honnête. Premièrement, la latence : parcourir le carnet tel qu'il se présentait après votre horodatage de décision, où est votre latence signal-vers-exchange mesurée — le carnet que vous avez vu n'est pas le carnet que vous avez touché. Deuxièmement, les fills partiels marketables : si la profondeur affichée dans votre prix limite-through est inférieure à , le modèle doit retourner un fill partiel et laisser un reliquat en attente, ce qui transmet le problème à la machine à états ci-dessous.
L'erreur résiduelle à l'échelon 3 est l'impact et le réapprovisionnement : vous consommez le carnet comme un objet statique, mais les carnets réels se réapprovisionnent partiellement (et les contreparties réelles réagissent). Pour des tailles de clip en dessous de quelques pourcents de la profondeur au meilleur niveau, cette erreur est faible ; pour des clips plus importants, il faut superposer un modèle d'impact (Almgren-Chriss 2001).
Échelon 4 : fills probabilistes selon la position dans la file d'attente pour les ordres limites
Les échelons 0 à 3 répondent à « à quel prix mon ordre agressif est-il fillé ». L'échelon 4 répond à la question la plus difficile : mon ordre passif est-il fillé, tout simplement — et c'est le seul échelon capable de valoriser une stratégie maker. Un ordre limite en attente au prix est fillé quand le volume tradé cumulé à dépasse le volume de la file qui se trouvait devant lui. Cela exige de suivre votre position dans une file FIFO que vous ne pouvez pas observer directement.
La mécanique d'estimation de la position dans la file d'attente — position initiale, règles de mise à jour sur trades vs. annulations, la famille de probabilité pour allouer les annulations non observées — est la primitive que nous avons construite dans Queue inside the wall. Je ne la re-dériverai pas ici ; le simulateur la consomme. Ce qu'ajoute l'échelon 4 est la règle de décision de fill par-dessus l'estimation, couverte dans la section sur la probabilité de fill ci-dessous.
L'importance de cet échelon est quantifiée dans la littérature : Moallemi et Yuan (2016), « A Model for Queue Position Valuation in a Limit Order Book », montrent que pour les instruments à gros tick, la valeur économique d'une position en tête de file par rapport à une position en fin de file est comparable au demi-spread — le même ordre de grandeur que l'intégralité de l'edge théorique d'une stratégie de market making. Un modèle de fill qui ignore la position dans la file n'estime pas mal le PnL d'un maker ; il estime le PnL d'une stratégie différente.
Il existe un échelon 5 — la simulation complète multi-agents où le marché réagit à vos ordres (modèles queue-reactive au sens de Huang, Lehalle et Rosenbaum 2015 ; frameworks multi-agents comme ABIDES, Byrd et al. 2020). Le replay historique, même avec prise en compte de la file d'attente, suppose que votre ordre ne change rien au comportement de tous les autres. Cette hypothèse est correcte à l'échelle retail et devient de plus en plus fausse à mesure que vos quotes deviennent une fraction visible du niveau. L'échelon 5 est hors périmètre ici ; sachez simplement que l'échelle ne s'arrête pas à 4.
Les fills partiels comme machine à états
Les échelons 0 à 2 peuvent faire semblant qu'un ordre est un appel de fonction : soumettre, obtenir un prix, terminé. À partir de l'échelon 3, un ordre est un processus avec un cycle de vie, et le simulateur doit le modéliser comme une machine à états, sinon il gérera silencieusement mal les cas qui comptent le plus.

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,
}
Les transitions portent l'économie du système :
PendingNew→Resting: l'ordre rejoint la file derrière tout ce qui est présent au moment de l'accusé de réception, pas au moment de la décision. Votre position dans la file est initialisée avec le volume du niveau tel qu'il était à . Les simulateurs qui initialisent au moment de la décision surestiment systématiquement la priorité dans la file — exactement du volume arrivé pendant votre fenêtre de latence, ce qui représente la majeure partie du volume lors des rafales.Resting→PartialFill: un trade à votre niveau, plus important que la file devant vous, vous fille partiellement. Le reliquat conserve sa position (désormais en tête de file). Les fills partiels ne sont pas du bruit — ce sont de l'information : être fillé à 0,3 sur 1,0 et voir le prix rebondir est un événement de PnL différent d'un fill complet, et le processus d'inventaire d'un maker se construit à partir de ces fragments.Resting→PendingAmend→Resting: le piège. Sur pratiquement toutes les plateformes crypto, un amend est un cancel/replace — lecancelReplacede Binance est atomique contre la double exécution mais retourne un nouvel ID d'ordre en fin de file. Même les plateformes disposant d'une sémantique de modification native (CME Globex) ne préservent la priorité temporelle que pour une diminution de quantité ; un changement de prix ou une augmentation de quantité la fait perdre. Donc dans le simulateur : tout amend de prix réinitialiseq_aheadau volume total actuel du niveau. Un moteur de cotation qui se repositionne toutes les 500ms ne « maintient » pas une cotation — il se réinsère perpétuellement en fin de file, et son profil de fill réaliste est presque de la sélection adverse pure. Combiné au résultat de Moallemi-Yuan : re-coter a un prix, et ce prix est votre position dans la file.PendingCancel→PartialFill→Canceled: les annulations prennent aussi de la latence. Dans la fenêtre entre la décision de retirer une cotation et l'arrivée de l'annulation au matching engine, vous pouvez encore être fillé — et ces fills sont les pires que vous recevrez jamais, parce que la raison pour laquelle vous retiriez la cotation est que le marché était sur le point de vous passer dessus. Un simulateur sans latence d'annulation supprime précisément les fills les plus toxiques de votre historique.
La machine à états est aussi ce qui rend la comptabilité du simulateur honnête : les frais s'accumulent par événement de fill, l'inventaire se met à jour par événement de fill, et les statistiques de temps passé dans chaque état (combien de temps les ordres restent en attente avant d'être fillés vs. annulés) deviennent directement comparables aux logs réels — ce qui alimente la boucle de calibration décrite en fin d'article.
Probabilité de fill sur ordre limite : trois modèles et une fourchette
Étant donné un ordre en attente au prix (disons un bid), quand le simulateur déclare-t-il un fill ? Trois règles de décision, par honnêteté croissante :
1. Touch-fill (naïf). Fill si le prix touche votre niveau : . C'est une règle de premier temps de passage (first-passage-time), et son échec a été mesuré il y a un quart de siècle : Lo, MacKinlay et Zhang (2002), « Econometric Models of Limit-Order Executions » (Journal of Financial Economics 65), ajustent des modèles de survie sur des données réelles d'ordres limites et concluent que les exécutions hypothétiques construites à partir des temps de premier passage « sont de très mauvais proxys pour les exécutions réelles d'ordres limites ». Le mode d'échec est structurel : quand le prix touche votre niveau et rebondit, le contact a consommé le devant de la file — les traders qui avaient coté avant vous. Le touch-fill vous attribue leurs fills. Pire, il vous attribue précisément les bons fills (touch-and-bounce est le scénario profitable pour un maker), alors que votre véritable ensemble de fills est biaisé vers le touch-and-run-through — ceux affectés par la sélection adverse.
2. Trade-through (borne conservatrice). Fill uniquement si le prix trade strictement à travers votre niveau : , ou sur données tick, le volume tradé cumulé à dépasse l'intégralité du niveau. Si le prix a tradé à travers, toute la file à a été consommée, donc vous étiez fillé quelle que soit votre position. Cela ne vous attribue jamais un fill que vous n'auriez pas obtenu. Son biais est l'image miroir de celui du touch-fill : il vous refuse tout fill où la file s'est vidée jusqu'à votre position sans pénétration complète, et les fills qu'il accorde sont disproportionnellement ceux de run-through (affectés par la sélection adverse). Un backtest maker sous trade-through est un stress test, pas une estimation.
3. Estimation par épuisement de la file. Suivre le volume tradé cumulé à votre prix à partir du tape de trades et le volume annulé déduit des deltas L2. Votre file estimée devant vous :
où est le volume du niveau à l'entrée de l'ordre (au moment de l'ack) et est la fraction des annulations supposée provenir de devant vous — le paramètre dont la forme rigoureuse (la famille ) est dérivée dans l'article sur la position dans la file. Le fill commence quand ; votre quantité fillée est le volume tradé en excès, ce qui produit naturellement des fills partiels :
Fixer (toutes les annulations devant vous) donne le bord optimiste de ce modèle ; le bord pessimiste. Quand vous n'avez que des snapshots L2 clairsemés et pas de tape de trades au niveau — courant avec les flux crypto throttlés à 100ms — vous pouvez vous rabattre sur un prior fondé sur un modèle : Cont, Stoikov et Talreja (2010), « A Stochastic Model for Order Book Dynamics » (Operations Research 58), modélisent chaque niveau de prix comme une file de naissance-mort (birth-death queue) et calculent, via des transformées de Laplace, la probabilité qu'un ordre au bid s'exécute avant que le mid ne bouge, conditionnellement aux tailles de file actuelles. C'est un oracle analytique de probabilité de fill : grossier comparé au replay du tape, bien meilleur que le touch-fill, et assez peu coûteux pour être évalué à l'intérieur d'une boucle de backtest critique en performance.
La discipline de la fourchette
Les trois règles ne sont pas des concurrentes — elles forment un ordre :
ce qui induit une fourchette de PnL. Exécutez chaque backtest maker trois fois et rapportez l'intervalle :
(l'inégalité supérieure est approximative — le touch-fill peut mal classer les stratégies, pas seulement gonfler leur PnL, parce qu'il vous attribue des bons fills contrefactuels). La règle de décision qui en découle : une stratégie maker n'est déployable que si elle survit au bord conservateur de la fourchette, et la fourchette doit être assez étroite pour que l'estimation ponctuelle signifie quelque chose. Une stratégie affichant +9 000 en trade-through a une fourchette large de 12 000 / +400$ vous dit quelque chose de réel.
L'expérience : descendre l'échelle avec une stratégie maker

Prenons un maker délibérément simple — cotations symétriques au meilleur bid/ask, clips fixes de 0,05 BTC, inventaire plafonné à ±0,5 BTC avec flattening taker au plafond. Un mois de données perpétuelles BTCUSDT : bougies 1m pour les échelons bas, diffs L2 100ms plus le tape de trades pour les échelons hauts ; frais maker 1,0 bps, taker 4,0 bps. Même code de signal à chaque échelon (noyau partagé, donc la seule variable est le modèle de fill). Les chiffres ci-dessous proviennent d'une de nos exécutions représentatives — vos magnitudes différeront selon la plateforme, le mois et la taille ; la forme, non :
| Échelon | Modèle de fill | Taux de fill des cotations | Fills | PnL du mois | Verdict |
|---|---|---|---|---|---|
| 0 | fill à la clôture | 98% | 41 200 | +14 800$ | fantaisie |
| 1 | ouverture bougie suivante / touch sur bougies 1m | 89% | 37 400 | +9 600$ | fantaisie avec latence |
| 2 | touch + spread & slippage fixe sur flattening taker | 89% | 37 400 | +7 100$ | coûts modélisés, fills toujours fictifs |
| 3 | + parcours de profondeur L2 sur flattening taker | 89% | 37 400 | +6 400$ | sorties honnêtes, entrées toujours fictives |
| 4a | trade-through (conservateur) | 21% | 8 900 | -3 900$ | borne de stress |
| 4b | épuisement de la file, calibré | 37% | 15 600 | -700$ | meilleure estimation |
| 4c | épuisement de la file, (optimiste) | 44% | 18 700 | +1 900$ | bord supérieur |
| — | run shadow live, même mois | 35% | 14 100 | -1 150$ | réalité |
Lisez le tableau de haut en bas et observez où la stratégie meurt. Ce n'est pas l'échelon 2 — frais et slippage retirent 26% et la stratégie a toujours l'air robustement profitable. Elle meurt entre l'échelon 3 et l'échelon 4, et elle meurt pour une raison qu'aucun modèle de coûts ne peut capturer : la sélection des fills. Le touch-fill accordait 37 400 fills dont la majorité était du touch-and-bounce — de la capture de spread pure. Le modèle tenant compte de la file a supprimé 58% de ces fills, et les fills supprimés étaient disproportionnellement les plus profitables : quand le niveau est tapé légèrement, la file devant un maker se re-cotant à latence retail absorbe tout. Les fills qui survivent jusqu'à l'échelon 4 sont biaisés vers les balayages qui vident le niveau — les fills où le prix est déjà en train de traverser votre position. Le taux de fill a chuté de 2,4x ; le PnL a changé de signe. Cette asymétrie — perdre les bons fills, garder les mauvais — est la sélection adverse rendue mécaniquement explicite, et elle est invisible à tout échelon inférieur à 4.
Notez aussi ce qu'a fait la fourchette : [-3 900] enjambe zéro, avec l'estimation calibrée à -700. Le simulateur n'a pas cloué le PnL live au dollar près — il a obtenu le signe, la magnitude, et le taux de fill à 2 points près. C'est à cela que sert un modèle de fill. Le backtest à l'échelon 1 a manqué le PnL live de 10 750$ sur une stratégie dont l'intégralité de l'edge brut mensuel n'était que de quelques milliers de dollars : l'erreur du modèle de fill était environ 3x la taille de l'alpha. D'où la thèse : votre modèle de fill est une hypothèse plus lourde que votre alpha.
Une mise en garde pour les échelons bas : si vous devez vivre avec des données en bougies (échelons 0-2), résolvez au minimum l'ambiguïté intrabougie avec le drill-down adaptatif — en descendant de 1m vers 1s/100ms/trades là où le SL, le TP, ou les niveaux de cotation tombent dans la plage de la bougie. Le drill-down corrige les erreurs de séquencement (quel niveau a été touché en premier) mais ne peut pas corriger les erreurs de file d'attente ; c'est le complément côté résolution des données de cet article, pas un substitut à l'échelon 4.
La boucle de calibration : se rapprocher des fills réels
Un simulateur d'échelon 4 a des paramètres libres — , la latence , la latence d'annulation, l'hypothèse de réapprovisionnement du niveau. Non calibré, ce n'est qu'une supposition de forme différente. La boucle qui en fait un instrument :
1. Loguer absolument tout en live. Chaque événement d'ordre avec horodatages exchange : soumission, accusé de réception, chaque fill partiel, accusés de réception d'amend, accusés de réception d'annulation. Plus l'état L2 au moment de la soumission. C'est la même discipline de logging que backtest-live parity exige pour son DivergenceMonitor — la calibration du modèle de fill est la couche la plus profonde de ce moniteur.
2. Rejouer les mêmes ordres à travers le simulateur. Injecter les données de marché enregistrées et les instructions d'ordres enregistrées (pas les fills) dans le simulateur. Vous obtenez maintenant des résultats appariés : pour chaque ordre live, un destin simulé.
3. Comparer les distributions, pas les moyennes, par bucket. Une simple concordance globale du taux de fill peut cacher des erreurs qui se compensent (trop optimiste en régime calme, trop pessimiste lors des rafales — se compensant en un résultat « calibré »). Segmentez par les facteurs déterminants :
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
Deux statistiques par bucket : l'écart de taux de fill signé (simulateur moins live) et un test KS sur le temps jusqu'au fill parmi les ordres fillés. La comparaison du temps jusqu'au fill est la plus révélatrice — un simulateur peut faire correspondre les taux de fill tout en fillant à des moments systématiquement erronés, ce qui corrompt toute statistique d'inventaire et de sélection adverse en aval. C'est précisément la leçon du cadrage en analyse de survie de Lo-MacKinlay-Zhang : l'exécution est un problème de temps-jusqu'à-événement, il faut donc la valider comme tel.
4. Ajuster les paramètres, dans l'ordre d'identifiabilité. D'abord la latence (mesurée directement à partir des horodatages d'ack — pas ajustée). Puis en minimisant l'écart de taux de fill à travers les buckets de profondeur de file. Puis vérifier les buckets de régime de volatilité : un écart optimiste persistant concentré dans les buckets de rafale signifie généralement que votre simulateur sous-modélise les fills toxiques dus à la latence d'annulation ou le réapprovisionnement du niveau, pas .
5. Relancer la fourchette. Après calibration, l'estimation par épuisement de la file devrait se situer à l'intérieur de la fourchette, proche du live, et — le véritable test d'acceptation — le classement des variantes de stratégie sous le simulateur devrait correspondre à leur classement en mode shadow. Ensuite, figer les paramètres et recalibrer selon un calendrier ; la dynamique des fills dérive avec les paliers de frais de la plateforme, les changements de tick size, et la population HFT, et un calibré en mars n'est plus qu'une hypothèse en juillet.
Attentes de convergence issues de nos runs : un simulateur d'échelon 4 non calibré se situe typiquement à ±10-15 points du taux de fill live ; après une passe de calibration, ±3-5 points, avec des p-values du test KS sur le temps jusqu'au fill qui ne rejettent plus uniformément. Vous ne ferez pas mieux avec du replay historique — le résiduel est la réaction du marché à votre présence, ce qui est le problème de l'échelon 5.
Ce qu'il faut retenir
- Nommez votre échelon. Chaque backtest se situe sur cette échelle, que vous ayez choisi l'échelon ou que votre framework l'ait choisi à votre place. Si vous ne pouvez pas nommer l'échelon de votre modèle de fill et ses biais connus, votre chiffre de PnL a des barres d'erreur que vous n'avez pas vues.
- Les takers peuvent s'arrêter à l'échelon 3. Le parcours de profondeur plus la latence mesurée valorisent honnêtement l'exécution agressive à l'échelle retail. Investissez l'effort économisé dans la qualité des données.
- Les makers commencent à l'échelon 4. En dessous, la logique de fill des ordres limites n'est pas approximative — elle sélectionne un ensemble contrefactuel de fills avec un biais de sélection adverse opposé. Les backtests touch-fill de stratégies maker sont le générateur le plus fiable de stratégies qui meurent au contact de la production.
- Modélisez le cycle de vie, pas le fill. Fills partiels, amend qui réinitialise la file, latence d'annulation — la machine à états est là où vivent les fills toxiques, et les fills toxiques sont là où meurt le PnL des makers.
- Rapportez la fourchette. Les bornes conservatrice et optimiste coûtent deux exécutions de backtest supplémentaires et convertissent « mon backtest dit +7k, +1,9k$] » — ce qui est une décision différente, et meilleure.
- Calibrez face aux fills réels par bucket. Un modèle de fill validé uniquement en agrégat est un modèle de fill avec des erreurs compensatoires cachées. Écarts de taux de fill par bucket plus tests KS sur le temps jusqu'au fill, recalibrés trimestriellement.
Les échelons de l'échelle ne sont pas des gradations académiques — chacun est un mensonge spécifique que votre backtest cesse de vous raconter. Montez jusqu'à ce que les mensonges soient plus petits que votre edge.
Liens utiles
- Moallemi, C., Yuan, K. — A Model for Queue Position Valuation in a Limit Order Book (2016)
- Cont, R., Stoikov, S., Talreja, R. — A Stochastic Model for Order Book Dynamics, Operations Research 58(3), 549-563 (2010)
- Lo, A., MacKinlay, C., Zhang, J. — Econometric Models of Limit-Order Executions, Journal of Financial Economics 65(1), 31-71 (2002)
- Huang, W., Lehalle, C.-A., Rosenbaum, M. — Simulating and Analyzing Order Book Data: The Queue-Reactive Model, JASA 110(509), 107-122 (2015)
- Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
- Byrd, D., Hybinette, M., Balch, T. — ABIDES: Towards High-Fidelity Multi-Agent Market Simulation (2020)
- Binance API — Cancel-Replace order semantics
- 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.}
}
Authors
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.