September 2, 2026 · microstructure

Your limit order didn't fill: a letter about queue position, maker backtests and the Sharpe you invented

Your limit order didn't fill: a letter about queue position, maker backtests and the Sharpe you invented

You sent over the notebook last week: same signal, same universe, same 14 months of BTCUSDT perp data. The only change was execution. You stopped crossing the spread and started resting bids one tick inside, and the Sharpe went from 0.42 to 2.14. You asked whether that's real or whether you broke something.

You broke something. I want to walk you through exactly where, because the bug is one line and the lesson underneath it is much bigger than the line.

Here's your fill rule, copied out of your engine:

if bar.low <= limit_price: fill(limit_price)

That says: if the market printed a trade at or below my price during this minute, I got filled at my price. What it actually encodes is that the price touched your level. Touching is not filling. Between those two things sits a queue, and you have modelled the queue as empty.

What's standing in front of you

When you post a bid at 84,120.0 on BTCUSDT perp, you join the back of whatever is already resting there. On the top-of-book level of that contract, that's usually somewhere between 4 and 30 BTC depending on time of day; the median in your sample window sits around 12 BTC. Your order is 0.4 BTC. For you to trade, sellers have to hit that level with enough aggregate size to clear everyone who arrived before you, and they have to do it before the level gets cancelled out from under you or the market lifts away.

So the question your backtest should be asking isn't "did price reach 84,120.0" but "did at least 12.4 BTC of market sells execute at 84,120.0 while my order sat there." Those are wildly different events. In your data, they differ by a factor of about three.

12.4 BTCmedian size ahead of you at touch
100%fill rate your backtest assumes
31%of touches that cleared your queue
4,180trades that became roughly 1,300

Three fill rules, three different strategies

I reran your signal with the same entries and three execution models. Same alpha, same fees, same funding. Only the fill logic changed.

Fill ruleFillsMean 60s edge after fillSharpe
Touch: low <= limit4,180+2.6 bp2.14
Strict penetration: low < limit - 1 tick1,712+0.4 bp0.61
Volume-at-level queue sim1,306+0.9 bp0.77

The middle row is the crude fix everyone reaches for first: only count a fill if the market traded strictly through your price, on the theory that if it went past you, it must have consumed you. That's directionally right and it kills most of the fantasy. It's also biased in a nasty way, which I'll get to.

The bottom row is the one you should build. It needs no L3 feed and no order-by-order reconstruction. You already have most of it.

A queue sim you can build from aggTrades

Pull the aggregate trade stream instead of klines. Every print carries price, quantity, timestamp, and the maker-side flag, which tells you whether the aggressor was buying or selling. That's enough for a serviceable simulation of your own passive order:

  1. When your order posts, snapshot the resting size at your price. If you only have a 100ms book feed, use the last snapshot; the error is small compared to the error you're fixing.
  2. Set queue_ahead = resting_size. Be pessimistic and assume you're last. You are, unless you're the one creating the level.
  3. Walk the trade stream. Every sell-aggressor print at or below your price decrements queue_ahead by its quantity. When it goes negative, you're filled, at your price, at that timestamp.
  4. If price moves a tick away, don't reset the queue to zero. Decay it. Some of the orders ahead of you cancel when the level goes stale, some don't. A 30-40% decay per full second off-touch matched my reconstructions better than either extreme.
  5. If your strategy would cancel and repost, model the repost as a new order at the back of the new level. This is the step people skip, and it's where the remaining fantasy hides.

Step 2 is where you'll want to argue with me. Yes, sometimes you're near the front, because you posted the moment the level formed. Fine — instrument it, don't assume it. Log the resting size at post time and let the data tell you what fraction of your orders are genuinely early. In your strategy it was 11%, because your signal fires after a move, which means the level you're joining already exists and already has a crowd on it.

The fills you get are the ones you wish you hadn't

Now the part that actually matters, and the reason the strict-penetration rule is biased.

Think about when your bid gets fully consumed. It happens when sell pressure is large enough to eat the whole level. That is, by construction, the moment when the market is moving down through your price. The fills you're most certain about are the fills where you're immediately offside.

Split your fills by how they happened and measure the 60-second mark-out:

Fill typeShare of fillsMark-out at 60s
Level traded, price bounced up38%+3.1 bp
Level traded, price flat21%+0.2 bp
Price traded through by 2+ ticks41%−2.4 bp

Your naive backtest gave you all three buckets and priced them all as free. The strict-penetration rule gives you almost exclusively the third bucket, which is why its edge collapsed further than the queue sim's. Neither is right. The queue sim gets you a realistic mix, and the mix is the whole game: passive execution earns you the spread and pays you back in adverse selection, and the ratio between those two is your actual strategy.

The old equities desk framing still holds up: a maker order is a free option you wrote to the market. Someone exercises it when it's worth exercising. Your backtest was collecting the premium and forgetting the option had a payoff leg.

The trades you didn't get change the strategy, not just the cost

This is the piece I most want you to sit with. When you model taker execution badly, you get the right trades at the wrong price, and a fee correction fixes most of it. When you model maker execution badly, you get the wrong set of trades entirely. Roughly 2,900 of your 4,180 entries never happened. Some of those were your best signals, on candles that spiked through and reversed, which is exactly the shape where the market ran away without you.

So your unfilled branch needs real logic. What does the strategy do when the entry doesn't fill by the time the signal is stale? Chase with a taker order and pay the spread plus impact? Repost lower and accept a different entry basis? Skip the trade and sit flat? Each choice produces a materially different equity curve, and none of them is "assume you were filled." In our runs, adding an honest chase rule (cross after 20 seconds unfilled, capped at 3 bp of slippage) recovered about a third of the missing trades and roughly half of the gap between the naive and queue-sim Sharpe. Which is a genuinely interesting result, and it only shows up once the fill model is real enough to make the question meaningful.

Cheap sanity check, ten minutes: take your live paper-trading log and your backtest over the same window. Compare fill rate, not PnL. If the backtest fills 100% of resting orders and paper fills 34%, you don't have a strategy comparison, you have a fill-model bug. Fix that before you look at a single return number.

Two smaller things while I have you

Post-only rejects. If you're using post-only to guarantee the maker fee tier and the book moves between your decision and the exchange's ack, the order comes back rejected rather than resting. In our paper logs that's 3-6% of attempts on BTCUSDT during normal hours and north of 15% in the minute after a US CPI print. A rejected order is not a filled order and it's not an unfilled resting order either; it's a trade that never existed, and if your backtest doesn't have that state, your trade count is inflated in precisely the regimes you care about most.

Self-trade prevention and your own footprint. At 0.4 BTC you're not moving BTCUSDT, so skip it. But you mentioned wanting to run this on a mid-cap alt perp where top-of-book is often under $15k notional. There, your order is a meaningful share of the queue, and the queue-ahead figure you snapshot includes the effect of your last order's presence. Once your size is more than about 10% of the resting level, the sim needs to account for the fact that other participants react to you, and honestly, at that point I'd trust paper trading over any simulation you or I can write.

Rerun it with the queue sim and send me the mark-out table split by fill type. If the bounce bucket still carries most of the edge and the through bucket isn't eating all of it, you may have something worth putting on paper. If the whole thing was the 2,900 fills you never would have gotten, better to learn it now than after four weeks of watching a paper account not do what the notebook promised.

limit ordersqueue positionadverse selectionbacktestingcrypto futures
← All posts