Latency Models in HFT Backtests Explained
Latency models in HFT backtests decide which fills are real. See how feed delay and order entry delay differ, and what a zero latency fill quietly hides.
Latency models in HFT backtests are the rules a replay engine uses to settle two questions: when your strategy could first have seen an exchange event, and when an order it sent could first have reached the matching engine. A market making simulation runs those as two separate clocks. Set both to zero and the engine hands you fills no real system could have won.
The two clocks a market making backtest has to model
Feed latency is the gap between the moment an exchange publishes an event, a book change or a print, and the moment your process receives it. Order entry latency is the gap between the moment your strategy releases an order and the moment the matching engine accepts it. A third clock, order response latency, covers the trip back for an acknowledgement or a fill message. The hftbacktest project, the open source reference implementation for this kind of simulation, separates all three in its documentation, and that separation is the whole game. Feed latency governs what your strategy is allowed to know. Order entry latency governs whether the price level you aimed at still exists when your order lands.
The two are not interchangeable. A colocated system can carry a feed latency of a few microseconds alongside an order entry latency ten times larger, since the outbound path crosses a risk gateway the inbound path never touches. One number standing in for both flattens the asymmetry that quoting strategies live inside. The mechanics of a full run are covered in how to backtest a trading strategy. This piece is about the clocks.
Why latency models in HFT backtests are a look ahead problem
Look ahead bias is usually taught with daily bars: a signal that uses the closing price to trade that same close. Zero feed latency is the identical error measured in microseconds. The strategy cancels on a print it had not yet received, or joins a level that was already gone from the book by the time its order could have arrived. What it books is a phantom fill, an execution the simulator grants against a state of the world the strategy could not have observed.
Phantom fills are quiet. Nothing crashes, no exception is raised, and the equity curve simply comes out smoother than the venue would ever have allowed. The scale of the exposure is easiest to see on the quote tape itself. The panel below takes every consolidated quote update in Apple stock during one mid morning hour, measures the gap back to the previous update, and reports the share of quotes that survived no longer than each latency budget.
| latency_budget | pct_replaced_within |
|---|---|
| 1 ms | 73.67 |
| 2 ms | 75.13 |
| 5 ms | 77.45 |
| 10 ms | 79.91 |
| 25 ms | 83.36 |
| 50 ms | 88.13 |
| 100 ms | 92.07 |
| 250 ms | 96.87 |
| 1000 ms | 99.93 |
The exact SQL behind every number
WITH gaps AS
(
SELECT
dateDiff('millisecond',
lagInFrame(sip_timestamp) OVER (ORDER BY sip_timestamp, sequence_number
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
sip_timestamp) AS gap_ms
FROM global_markets.cache_stocks_quotes
WHERE ticker = 'AAPL'
AND sip_timestamp >= '2026-09-15 14:00:00'
AND sip_timestamp < '2026-09-15 15:00:00'
AND bid_price > 0
AND ask_price > bid_price
)
SELECT
concat(toString(budget_ms), ' ms') AS latency_budget,
round(100 * countIf(gap_ms BETWEEN 0 AND budget_ms) / count(), 2) AS pct_replaced_within
FROM gaps
ARRAY JOIN [1, 2, 5, 10, 25, 50, 100, 250, 1000] AS budget_ms
GROUP BY budget_ms
ORDER BY budget_msRead the curve from the left. 73.67% of quotes were replaced within 1 ms of arriving, and at a budget of 1000 ms the figure reaches 99.93%. A simulator that lets a strategy act on a quote with no delay is letting it trade the far left of that curve, the region where most of the book has already moved on.
How fast does the top of book actually move?
Speed is not uniform across names, and a single latency assumption spread over a portfolio ignores that. The next panel runs the same measurement across five household tickers over the same hour, at two budgets: five milliseconds and fifty.
| symbol | replaced_within_5ms_pct | replaced_within_50ms_pct |
|---|---|---|
| SPY | 81.2 | 95.8 |
| AAPL | 77.4 | 88.1 |
| NVDA | 76.6 | 93.4 |
| KO | 72.8 | 83.9 |
| MSFT | 71.4 | 81.9 |
The exact SQL behind every number
WITH gaps AS
(
SELECT
ticker,
dateDiff('millisecond',
lagInFrame(sip_timestamp) OVER (PARTITION BY ticker ORDER BY sip_timestamp, sequence_number
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
sip_timestamp) AS gap_ms
FROM global_markets.cache_stocks_quotes
WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'SPY', 'KO')
AND sip_timestamp >= '2026-09-15 14:00:00'
AND sip_timestamp < '2026-09-15 15:00:00'
AND bid_price > 0
AND ask_price > bid_price
)
SELECT
ticker AS symbol,
round(100 * countIf(gap_ms BETWEEN 0 AND 5) / count(), 1) AS replaced_within_5ms_pct,
round(100 * countIf(gap_ms BETWEEN 0 AND 50) / count(), 1) AS replaced_within_50ms_pct
FROM gaps
GROUP BY ticker
ORDER BY replaced_within_5ms_pct DESCSPY tops the panel: 81.2% of its quotes were gone inside five milliseconds. MSFT sits at the other end with 71.4%. Five milliseconds of modelled delay is a different penalty in each of those books, and one global constant charges the same toll to both.
What does a millisecond of delay actually cost?
Delay carries a price that is measurable without any strategy attached. Take the mid price, the midpoint between the best bid and the best offer, sample it into 100 millisecond slots, and ask how far it has travelled after each horizon. That distance is the head start a faster participant holds over a slower one, and it is the raw material of adverse selection: the tendency for your resting quote to be filled precisely when the price is about to move through it.
| latency_horizon | avg_mid_move_bps |
|---|---|
| 0.1 s | 0.224 |
| 0.5 s | 0.591 |
| 1.0 s | 0.877 |
| 5.0 s | 2.232 |
The exact SQL behind every number
WITH grid AS
(
SELECT
intDiv(toUnixTimestamp64Milli(sip_timestamp), 100) AS slot,
argMax((toFloat64(bid_price) + toFloat64(ask_price)) / 2, sip_timestamp) AS mid
FROM global_markets.cache_stocks_quotes
WHERE ticker = 'AAPL'
AND sip_timestamp >= '2026-09-15 14:00:00'
AND sip_timestamp < '2026-09-15 15:00:00'
AND bid_price > 0
AND ask_price > bid_price
GROUP BY slot
),
lagged AS
(
SELECT
mid,
lagInFrame(mid, 1) OVER w AS mid_100ms_ago,
lagInFrame(mid, 5) OVER w AS mid_500ms_ago,
lagInFrame(mid, 10) OVER w AS mid_1s_ago,
lagInFrame(mid, 50) OVER w AS mid_5s_ago
FROM grid
WINDOW w AS (ORDER BY slot ASC ROWS BETWEEN 50 PRECEDING AND CURRENT ROW)
),
moves AS
(
SELECT *
FROM lagged
WHERE mid_100ms_ago > 0 AND mid_500ms_ago > 0 AND mid_1s_ago > 0 AND mid_5s_ago > 0
)
SELECT
horizon.1 AS latency_horizon,
round(10000 * avg(abs(horizon.2)) / avg(mid), 3) AS avg_mid_move_bps
FROM moves
ARRAY JOIN
[
('0.1 s', mid - mid_100ms_ago),
('0.5 s', mid - mid_500ms_ago),
('1.0 s', mid - mid_1s_ago),
('5.0 s', mid - mid_5s_ago)
] AS horizon
GROUP BY latency_horizon
ORDER BY avg_mid_move_bps ASCOver that hour the mid travelled an average of 0.224 basis points across 0.1 s and 2.232 basis points across 5.0 s. A basis point is one hundredth of one percent. Set that against a quoted spread of a basis point or two on a large cap, and a latency model that is a full second too optimistic is understating adverse selection by something close to the entire spread it is trying to earn.
Constant, sampled, or feed derived latency
A replay engine exposes the choice as a latency model you attach to the run. The hftbacktest documentation for the tagged v1.8.4 release names several shapes, and each distorts the truth in its own direction.
- A constant model applies one fixed entry latency and one fixed response latency to every order. It is the easiest to reason about and the easiest to fool yourself with, since real latency has a long right tail and the worst delays cluster in the busiest moments.
- Feed derived models compute order latency from the feed latency the replay just measured, scaled by a multiplier plus a base offset. The backward variant uses the most recently observed feed latency, the forward variant uses the next one, and a third averages the two. The forward variant reads a value from ahead of the current message, which the documentation states plainly as forward looking information, so it inherits a mild version of the bias this whole piece is about.
- An interpolating model reads recorded order latency data, real round trips captured against the venue, and interpolates between the samples. The documentation calls it the most accurate of the provided models when the recorded data is fine grained.
- A custom model, any object exposing an entry method and a response method, lets you replay a distribution you measured yourself rather than a shape someone else assumed.
What each one gets wrong is as useful as what it gets right. Constant latency understates the tail and gives you a trader whose bad days never arrive late. Feed derived latency ties the order path to feed congestion, a fair proxy, though the multiplier is a parameter you are choosing rather than measuring. Interpolated round trips are the most faithful of the four and the least portable: they describe one venue, one cabinet, one set of session hours, and they do not transfer to a different exchange. Reconstructing the inbound clock from packet timestamps is the same discipline covered in packet capture market data replay. Whichever you pick, pin your reading to a tagged release rather than main, since model names and constructor arguments move between versions.
Why one latency number misprices the open
Event rate swings hard across a session, and a constant model charges the same delay at every point on that curve. The panel below counts trade prints per second in five minute buckets across the full regular session on the same pinned date.
| et_time | trades_per_second |
|---|---|
| 09:30 | 109.5 |
| 09:35 | 48.6 |
| 09:40 | 37.5 |
| 09:45 | 38.7 |
| 09:50 | 31.7 |
| 09:55 | 37.2 |
| 10:00 | 45.4 |
| 10:05 | 42.2 |
| 10:10 | 41.1 |
| 10:15 | 36.9 |
| 10:20 | 41.2 |
| 10:25 | 38.7 |
| 10:30 | 44.7 |
| 10:35 | 39.4 |
| 10:40 | 45.6 |
| 10:45 | 35.7 |
| 10:50 | 38.9 |
| 10:55 | 44.6 |
| 11:00 | 38.2 |
| 11:05 | 36.6 |
The exact SQL behind every number
SELECT
formatDateTime(toStartOfFiveMinute(toTimeZone(window_start, 'America/New_York')), '%H:%i') AS et_time,
round(sum(transactions) / 300, 1) AS trades_per_second
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'AAPL'
AND window_start >= '2026-09-15 13:30:00'
AND window_start < '2026-09-15 20:00:00'
GROUP BY et_time
ORDER BY et_timeThe opening bucket at 09:30 printed 109.5 trades a second. The closing bucket at 15:55 printed 112. The middle of the day sags well below both, and the panel draws that U across 78 buckets. Gateways, wires and matching engines are all under their heaviest load at the two ends of that shape, which is exactly where a single flat delay parameter is least believable.
Queue position and latency are separate problems
Databento published a queue position tutorial on 26 February 2025 that works the other half of the fill question. It estimates where a resting order sits in the queue from L2 data, and it parameterizes cancellation bias with a factor k between -1 and 0, where k = 0 is the naive assumption that cancels are spread uniformly through the queue. Their measurement lands elsewhere: cancels come mostly from the back. That matters for fills, since a cancel ahead of you advances your order and a cancel behind you does not.
That tutorial models cancellation bias and does not model latency, which is no flaw in it. The two are complementary halves of an honest fill simulation. Queue position answers whether the trades printing at your price ever reached your order. Latency answers whether your order was in the queue at all at the moment those trades arrived. Get the first from how to estimate queue position, and check what your replay is actually reading against MBO versus MBP order book data, since queue modelling from aggregated levels is an estimate while message by message data is a record.
How these panels were measured
Every quote panel reads the consolidated US quote tape for one pinned session, 15 September 2026, between 10:00 and 11:00 New York time, so the figures stay fixed on re-run rather than sliding with the latest day. Gaps are measured between consecutive updates on the same symbol in sequence order; the first update of the window has no predecessor and is never counted as replaced. The mid price panel keeps the last quote in each 100 millisecond slot and compares it with the slot 1, 5, 10 and 50 steps back, so slots with no quote at all are absent from the series and the longer horizons read as a lower bound on elapsed time. The session panel counts trade prints from one minute bars across the 09:30 to 16:00 regular session on that same date.
FAQ
What is feed latency in a backtest?
Feed latency is the delay between an exchange publishing an event and your system receiving it. Inside a replay engine it sets the earliest moment your strategy is allowed to act on each message, which is why a run with zero feed latency can quote against prints it never could have seen.
What is a phantom fill?
A phantom fill is an execution the simulator grants that a live system could not have won. It usually comes from a model that let the strategy cancel or join using information that had not yet reached it, or that let an order arrive at a price level already gone from the book.
Does a slow strategy need a latency model?
The delay matters in proportion to the holding period. A position held for days is barely touched by a few milliseconds of order entry latency. A quoting strategy that rests at the top of book and pulls on every change lives entirely inside that window, so the model choice moves its whole result.
How is real order entry latency measured?
By timestamping the outbound request and the venue acknowledgement against the same clock, then keeping the full distribution rather than the mean. Capturing the inbound feed at the network layer supplies the other clock, and the two recordings together are what an interpolating latency model consumes.
Is queue position or latency the bigger source of error?
They break a simulation in different places. Latency errors misstate which orders existed at the venue and when. Queue errors misstate which of the orders that did exist got filled. A backtest that fixes one and ignores the other still reports fills it could not have earned.
Every panel above ships with the SQL that produced it. Open one, swap the ticker or the hour, and run it on the Strasmore terminal to see how these curves look for the name you are quoting.