Tick replay with a queue model
Replay a recorded tape and make a resting maker order wait its turn — seeded from the book, gated by trades, and held long enough that the round trip could have happened.
The default backtest fills a resting limit the instant price touches it. crates/vike-backtest/src/queue_model.rs says what that means and why it is wrong for a maker: the frozen crossing model behaves as if the order were always first in the queue, so it over-fills. On a real venue an order at price P fills only after the resting size ahead of it at P has been consumed. This tutorial turns that estimate on.
The profile
Everything here is tick mode. Two knobs and a floor:
[data]
kind = "tick"
from = "1775000000000"
to = "1775002000000"
[[data.series]]
venue = "binance"
symbol = "BTCUSDT"
[engine]
cash = 10000.0
slippage = 0.0
fill_model = "l2book"
queue_model = "risk_adverse"
queue_seed_depth = 0.0
queue_min_hold_ms = 2000queue_model, fill_model, queue_seed_depth, queue_min_hold_ms, feed_latency, order_latency_ms, fill_latency_ms and equity_sample_every are tick-mode only. A bar profile that sets queue_model is rejected by BacktestProfile::validate rather than silently ignored, and an unrecognised value is a fail-fast validation error at load — a typo like "l2_book" must not quietly select the optimistic model and undo the realism knob. Absent, the whole lane is off and the run is byte-identical to the frozen crossing fill.
What the gate actually does
crates/vike-backtest/src/engine/queued.rs is reached only when EngineParams::queue_model is Some; a default run never enters that module at all. The composition rule is stated in the queue model's own header and is worth reading as a sequence, because the queue gate layers on top of the price law rather than replacing it:
- the existing fill model still decides the price condition (limit crossed or touched);
- a strict cross — a trade or quote that trades through the level — fills in full: everyone at the level, the queue ahead included, was consumed;
- a touch at exactly the order's price is queue-gated. A trade at P first consumes the estimated front, and only its excess beyond the front fills the order, producing partial fills; a quote touch fills only once the front is already cleared;
- recorded L2
BookUpdates driveon_depth_changeat each tracked level, so cancellations ahead of the order shorten the wait — the depth-consumption channel the crossing model ignored entirely.
That last point is the reason to record the book and not just the trade tape. queue_seed_depth is the fallback front used when neither the replayed book nor an L1 quote gives a size at the order's price, and its default is 0.0 — a quote with no observed size-ahead fills on the first at-price trade, which is the optimistic answer again. Feed the book, or set the floor deliberately.
Choosing a model
Three values, case-insensitive:
risk_adverse— the conservative bound. The front shrinks only on hard evidence: trades at the order's price, or the level itself shrinking below the current front estimate. Cancellations behind the order are never credited, so this is the pessimistic, slowest-fill estimate.prob_power/prob_power:N— probabilistic, with weightf(x) = x^N. A net depth decrease is split between the queue ahead and behind byprob = f(back) / (f(back) + f(front)), the probability the cancellation came from behind.N = 1splits a decrease pro rata by size; largerNbiases attribution toward the (usually larger) back.prob_log— the same split with the sub-linear weightf(x) = ln(1 + x), so large backs saturate.
Two of the three arms are platform-sensitive
ProbFunc::f calls libm::pow and libm::log — never x.powf(n) or .ln(), which resolve to the platform's own libm and disagree in the last bit between a Windows CRT and glibc. What that bit decides here is a fill: the weight moves the estimated queue ahead of a resting maker, which moves whether a trade's excess reaches the order at all, so a backtest can fill on one box and not the other. Decision 0032 carries the measurement. The bare "prob_power" spelling escapes it — it parses to Power(1.0) and libm's pow returns x unchanged for y = ±1 ahead of any arithmetic — so the unconditional exposure is "prob_log" plus "prob_power:N" for any N ≠ 1.
The minimum hold
queue_min_hold_ms defers a position-reducing fill on the tagged maker lane until that long after the position opened, so the backtest cannot fabricate a sub-second maker round trip; the untagged queued-limit lane runs no such check at any setting. It is consulted only when queue_model is set, defaults to 0 (off), and its own doc names where to get the number: calibrate from real market-making flip times — the reference measurement is a Polymarket BTC-5m ≈ 2 s at the 10th percentile. A maker result with no floor at all is a result that assumes it could re-quote instantly.
Pair fill_model = "l2book" with slippage = 0.0: that model fills a resting order only up to the displayed book depth and rests rather than filling size the market never showed, so the book walk is the slippage and a flat rate on top double-counts.
Verify it
Run the same profile twice, once with queue_model absent and once set, and compare n_trades and final_equity. The difference is the whole point, and it should be large for a passive strategy. Then read the honesty note the shipped crates/vike-backtest/profiles/wf_grid.toml writes about its own bar-mode grid, because it names the ceiling this page exists to lift:
NO QUEUE POSITION, AND NO WAY TO ADD ONE ON THIS LANE… a bar-mode grid fills EVERY rung the bar's high/low merely TOUCHED, at its own limit price, in full size. Real passive fills happen only when someone crosses you, and the ones you get are adversely selected. This is the single largest optimism in the run and it cannot be modelled here — a tick/book profile with
queue_model+queue_min_hold_msis the only honest version of a maker backtest.
Next: queue models and the queue lane for the reference pages, latency models for the reaction-time half, and record and replay for getting a book on disk in the first place.
The A-S maker on a paper book
Mount the Avellaneda-Stoikov SpreadMaker on the production live core with a paper exchange behind it, and see why the tick-to-bar bridge is the piece that makes it fill.
Record a feed, then replay it
Accumulate your own market tape with the headless recorder, stop it without losing the buffer, and replay the result deterministically into a backtest.