Quantitative Trading6 min read

Every Trade in My Backtest Filled

S

Suneet Malhotra

Jul 30, 2026

β€’
2 views
Every Trade in My Backtest Filled - Quantitative Trading blog post
πŸ”§PythonπŸ”§OptionsπŸ”§BacktestingπŸ”§Market Microstructure

Every trade in my options backtest filled. Not most of them. All of them. There is no line in that file where an order fails to execute, because there is no line where an order exists.

Here is the entry price for the credit spread, straight out of backtest.py:

credit = put_px(S0, Ks, T, iv_sell) - put_px(S0, Kl, T, iv_sell)

Two model prices, subtracted. That difference becomes the credit received, and every profit and loss figure in the report is computed against it. No counterparty, no queue, no possibility of the number being refused.

The docstring at the top of that file lists what the model leaves out, and it is honest about this one. No skew, no bid-ask, no early assignment, no dividends. I wrote that list and believed it was complete disclosure. It is not. Listing bid-ask next to dividends files it as a missing cost, and a missing cost has an obvious remedy: subtract more. That framing kept me from looking at it for months.

The bid-ask spread is not the missing piece. The missing piece is the event.

What the order actually is

In the live engine the same trade is one instruction:

order_class: mleg, type: limit, limit_price: one net number, legs: [short put, long put]

The backtest priced the two legs independently and added them. The order cannot do that. It names a single net credit for the package, and the package either trades at that net or does not trade at all. That is not a detail about request formatting. It changes where the order goes.

Where a net limit actually rests

A spread order does not walk the two leg books taking whatever sits there. On Cboe BZX Options it rests on a separate Complex Order Book, and on entry a net limit priced at or through the Synthetic Best Bid or Offer, derived from the leg quotes rather than quoted directly, can start a Complex Order Auction instead of executing.

The rules for who gets filled are published, and they are not first come first served. The Complex Order Book allocates by price, then time. The auction allocates by price, then pro rata, meaning proportionally by size rather than by arrival. Customer liquidity providing orders execute ahead of everything else at the same price, regardless of size or time. Those are BZX rules; allocation differs by exchange, and I neither choose which one my order reaches nor record which one filled it.

The part that is signed

One thing I can state with high confidence. Among the spreads that do fill, the credit I receive is at or below the credit I asked for, never above it. That is not a market observation, it is arithmetic on how a limit order works. When my net is generous to me, fewer participants want the other side, and I sit unfilled. When I want the position anyway, I lower the credit until someone takes it. The prices I transact at are drawn from the unfavorable tail of the prices I request, and the backtest uses the request.

The second effect I am less sure about, and I would rather say so than dress it up. Whether the signals that failed to fill were the better ones has an unknown sign for this strategy. The mechanism is real: my model price and the market price disagree most when my volatility input is stale, which is exactly when a market maker declines my number. But I cannot tell from my own records which way that runs, and I will not assert a direction I have never measured.

Why charging more does not repair it

The instinct is to assume half the spread, call the backtest conservative, and move on. Right sign, wrong shape. A haircut penalizes every trade equally and leaves the trade count untouched, while the real mechanism worsens the price on some trades and deletes others outright. A pessimistic backtest with the original trade count still describes a strategy that got into every position it wanted, at a price nobody had to agree to.

The comment that contradicts its own code

Reading the order path for this post I found something I cannot resolve. The function that sends the spread carries this in its docstring:

Alpaca uses negative limit_price to indicate net credit on multi-leg.

Three lines below, the code sends the absolute value when the side is credit and the negated value when the side is debit. Comment and code state opposite conventions. One of them is wrong and the file does not say which.

What matters is not the sign, it is the failure mode. A wrong limit price does not raise. It produces an order that gets rejected, or one that rests until the close and expires having done nothing. Both are silent, and neither appears in my records, because what I persist is fills.

I do not have a table of orders. I have a table of trades. Every row in it is an order that worked.

What is worth building instead

The tempting fix is to model a fill probability and weight each backtest trade by it. I am rejecting that, for the same reason I rejected modeling assignment two days ago. It means estimating what other people will decide from data I do not hold, which is not knowledge. It is false precision with a decimal point on it.

The cheap fix generates information I do not currently have. Persist the order, not the outcome. Every submission, with the net credit requested, the timestamp, and the terminal state: filled, partially filled, rejected, or expired unfilled. When filled, the realized net beside the requested one.

Two numbers fall out of that table for free. The fraction of intended positions I actually get, and the average distance between the credit I ask for and the credit I receive. The second is what the backtest should use in place of a model price. Neither is recoverable from a fills table at any sample size, because the rows are not there to be found.

The cost, since a recommendation without one is not a recommendation: it changes nothing about what the account earns this month, produces no usable estimate for weeks, and looks like progress to nobody, including me.

The reason this one hid

Every other approximation in that backtest is an error in how well a trade did. Wrong volatility input, missing skew, held to expiry with no management. Errors in that class share a property I had stopped noticing: measuring them harder makes the number worse. They announce themselves as disappointment, and disappointment gets investigated.

This one is an error in which trades existed. It does not get worse under examination, because there is nothing to examine. An order that never filled leaves no row anywhere in the system. The evidence was never collected, and the collector was never written.

A cost I mis-estimated is a number I can go back and correct. A fill I never got is a row I never had. The first makes the report wrong. The second makes it unfalsifiable, and I have been quoting it for months.

Share this post

You Might Also Like

Stay in the Loop

Get weekly insights on AI-driven QA, engineering leadership, and automation strategies.

No spam, ever. Unsubscribe anytime.