Latency models
Order latency on the tick/book replay path — the entry and response legs, the constant and recorded-series models, the venue-enforced holds, what the response leg delays and what it deliberately does not.
Without a latency model a simulated order is submitted, matched and cancelled with zero
delay. crates/vike-backtest/src/latency.rs states the consequence in its header: the strategy
sees a tick and its order is already resting at the matching engine on that same tick, and a
cancel issued on the tick that would have filled it always wins. That gives a maker impossible
reflexes and is, the header says, the single largest source of optimism left in the replay path
after the queue model, its natural partner.
It is opt-in and replay-only: EngineParams::latency_model = Some(kind) arms it, None (the
default) means no in-flight queue is ever built and every code path is byte-identical, and only
StrategyEngine::run_ticks arms it — SimBroker::latency's doc says the gate is installed for
the duration of a tick replay and taken out at the end, which is what structurally guarantees the
bar engine and the vector kernel can never consult one.
Two legs
- Entry latency — a local submit at
Tbecomes visible to the matching logic atT + entry(). Everything the strategy sends — new orders, cancels, amends — is held in an in-flight queue keyed by that delivery timestamp. - Response latency — an exchange-side event at
exch_tsreaches the strategy atexch_ts + response(). Fills are delivered toStrategy::on_filllate.
Both are signed nanoseconds, while the engine's own timestamps are epoch milliseconds. The
wire-in keys its queue in nanoseconds (ts_ms × 1 000 000 + latency_ns) so a sub-millisecond
latency is not silently truncated; it simply means the action lands on the next tick whose
millisecond stamp has caught up. A negative entry latency encodes a recorded rejection: the
request never reached the matching engine, so the engine drops the action outright and records it
in SimBroker::dropped under "latency_reject" — the channel the
zero-trade analyzer reads, so a calibrated series
with a rejection burst cannot silently swallow a maker's orders and leave "why did my maker stop
quoting" undiagnosable.
The two shipped models
LatencyModel is the seam — entry(ts, order) and response(ts, order), pure functions with
no I/O, no clock and no interior mutation, so a replay is reproducible. LatencyOrder is the
deliberately tiny descriptor a model may condition on: side, quantity, optional price.
ConstantLatency is fixed nanoseconds on both legs, the calibration-free baseline. Its doc
draws a distinction worth keeping: entry_ns: 0 / response_ns: 0 reproduces zero-latency
behaviour while still exercising the in-flight queue, so it is not the same thing as
latency_model: None — an action submitted at T still applies only at the next drain, the
next tick at ts >= T.
IntpOrderLatency interpolates from a recorded series of round trips —
LatencyRow { req_ts, exch_ts, resp_ts } in epoch nanoseconds, exch_ts <= 0 marking a
rejection. Lookup is a binary search for the bracketing pair and a linear interpolation between
them, along a different abscissa per leg: the entry leg in req_ts over every row, the response
leg in exch_ts over a second index with rejection rows excluded. Outside the series the nearest
end row's latency is used, never an extrapolation, and rejections are not interpolated across —
if either bracketing row is one, the nearer row's latency is returned verbatim, so a recorded
rejection is never blended into a merely slow accept. An empty series is inert.
The in-flight queue
LatencyGate holds the built model and the queue. Every price tick drains everything whose
delivery stamp has arrived, so the queue only ever holds the actions issued within one latency
window; the due slice is sorted by (delivery_ns, seq), where seq is a monotonic submission
counter that keeps a batch sharing one delivery stamp in the order the strategy issued it — a
cancel then a re-submit must not swap. InFlight::seq's doc names the precondition: both shipped
models ignore the order descriptor, so that holds; a user model that gives a bare cancel a
different entry latency than the sized re-submit after it can legitimately reorder the pair, and
a maker whose requote's cancel lands last is left with its tag pulled rather than re-priced.
There is deliberately no end-of-run flush of the order queue: an action undelivered when the
tape ends never reached the venue and must not retroactively apply. The response leg is
flushed — flush_deferred_fills delivers every still-held fill, because a backtest must not end
with fills the strategy was never told about.
What the response leg delays, precisely
Only the strategy's view is delayed, never the exchange's own state. SimBroker::apply_fill
folds a fill into the position, cash and the equity curve the instant it matches, and
BacktestResult is computed from that truth. What the response leg holds back is the
Strategy::on_fill callback and the strategy-visible shadow position — a per-symbol signed
size advanced only by delivered fills, which Broker::position and HftBroker::position read
while the gate is armed. SimBroker::shadow_pos's doc says why it exists: a maker that polls its
inventory rather than accumulating it in on_fill used to see every fill with zero latency,
which made the leg inert for the spread maker, whose inventory skew and
reservation price are driven by exactly that poll.
Still bypassing the model, by design: SimBroker::position_of, pending_of, equity_now /
Broker::equity, drawdown_now and the target-percent verbs read exchange truth
synchronously — they are the accounting surface, and shadowing them would make on_stop disagree
with the result. A strategy that sizes off equity() still reacts to a fill with zero response
latency.
An order in flight is invisible to pending_of until delivered. SimBroker::in_flight_of(symbol)
is the read that sees it — always 0 when the gate is not armed — so a strategy checks
pending_of(s).is_empty() && in_flight_of(s) == 0 rather than re-firing a duplicate on every
tick of the entry-latency window. The pre-trade leverage cap does count in-flight market orders
(LatencyGate::in_flight_market_signed): a local risk check must model the local view, or every
submit inside one latency window would be granted the full leverage room in exactly the regime
the model exists to make pessimistic.
Venue-enforced holds
Some delays are venue rules, not network estimates. VENUE_HOLD_POLYMARKET_UPDOWN_MS is 250 ms,
verified against the venue's published order-lifecycle documentation on 2026-07-23: on exactly
the crypto up/down markets the venue holds the order, validates it again, and only then matches
or books it. VENUE_HOLD_POLYMARKET_SPORTS_GAME_MS is 3 000 ms, an independent mechanism
measured live the same day — 400 of 400 open sports markets carrying a game_start_time
declared seconds_delay: 3 — and its doc warns it is not universal, a later sweep having found
the esports book declaring 1 000 ms. Both literals are owned by vike_model::venue_hold.
LatencyModelKind::venue_hold_ms(ms) models a hold on the entry leg only — a hold delays when
the venue acts, not when the strategy hears about it — and a deployment pays it on top of its
wire round trip. Better than a constant, since the hold is a per-market property and one tape
can carry both kinds: SimBroker::taker_hold_table_ns resolves each symbol's
SymbolProperties::taker_hold_ms once at run start through the same point-in-time
properties seam the fill grid uses, and
LatencyGate::submit adds it to the model's entry leg; an empty or all-zero table is
byte-identical to no table. Paired with the L2 fill model, the
venue's "matched or booked" second validation falls out: decided on the book at T, delivered
at T + hold, matched against the book as it then stands, resting if it no longer crosses.
In a profile
A run profile exposes the constant model as two millisecond
knobs in [engine], tick mode only: order_latency_ms is the entry leg on every order action —
place, modify, cancel — and fill_latency_ms the response leg before the strategy learns of a
fill. Either > 0 builds a LatencyModelKind::constant; both 0 is None, byte-identical. A
recorded series is reached through EngineParams directly.
Market impact
The opt-in size-dependent slippage model for bar-mode backtests — the Almgren–Chriss cost function, its published constants and units, what it charges and what it deliberately does not, and where its output is bounded.
Tick replay
How recorded quote, trade and book rows become the replay stream a strategy sees, and the integrity rule the replayed book obeys.