At 10:07 on a Tuesday, our research agent flagged a short-term bullish setup in an index ETF. The underlying signal was ordinary: price had reclaimed its 20-bar average, realized volatility was easing, and the next session had a scheduled macro release. The proposed trade was a call spread with roughly two weeks to expiration.
The backtest liked it. The paper strategy did not place it. When we compared the orders, the signal timestamps matched and the underlying prices were within a few cents. The contracts were different: the backtest had bought the Friday expiration that looked best in the historical chain; the paper process selected the next listed expiration, one week later.
That gap came from a calendar join. We had treated “days to expiration” as a property of a date, then selected the contract whose expiration landed closest to the target. In the historical data, the expiration calendar included a weekly listing that had been added after the date we were testing. The backtest had used a contract from the future in a way that looked like a harmless convenience.
We first suspected a timezone bug. Options expiration dates are especially good at producing those: the market session ends in New York, exchange timestamps may be stored in UTC, and a midnight date can shift when a pipeline casts it carelessly. We checked. The timestamps were sound. The contract simply did not exist in the historical universe on that Tuesday.
That was the first lesson: a historical option chain needs a point-in-time listing universe. A contract’s expiration date tells you when it ends. It does not tell you when it was listed, when it became tradable, or whether your system could have selected it at the time.
We rebuilt the selection step around the information available at the decision timestamp. For every candidate contract, we required a listing timestamp no later than the decision, a valid quote no older than our freshness threshold, and a market session in which the strategy could submit an order. We also recorded the candidate set before ranking it. That last part mattered because the old backtest retained only the winner; we could not tell whether it had compared five eligible contracts or quietly chosen from a calendar that had not yet arrived.
Next we examined the quote used to price the spread. The backtest had sampled each leg’s latest quote independently. The call’s ask was from 10:06:58; the short call’s bid was from 10:04:11. Both were technically recent under the broad data policy, but the market had moved between them. A spread midpoint built from asynchronous legs can describe a market that never existed.
We changed the quote rule to require both legs to have timestamps within two seconds of each other and to reject crossed or zero-size markets. That reduced the number of usable historical observations. It also made the remaining marks more believable. When no synchronized quote existed, we marked the candidate unavailable instead of filling the gap with a stale price.
Then came the ranking rule. The agent had chosen the expiration with the lowest quoted spread cost. That sounds sensible until the spread is one contract wide and the quote is stale. We made the objective explicit: maximize expected payoff per unit of defined risk among contracts that pass minimum quote-size, freshness, and listing checks. For this example, the nearest eligible expiration had a $0.42 quoted width across the two legs, while the later expiration had a $0.31 width. The cheaper-looking choice also had only one contract displayed on the short leg.
| Candidate | Expiration available then? | Quote age | Displayed short-leg size | Decision |
|---|---|---|---|---|
| Friday weekly | No | Not applicable | Not applicable | Exclude |
| Following Friday | Yes | 1.4 seconds | 18 contracts | Eligible |
| Third Friday | Yes | 9.8 seconds | 1 contract | Exclude as stale and thin |
We replayed the incident with those rules. The backtest now chose the same expiration as paper and recorded why the other contracts were rejected. The resulting spread entry was worse than the original historical fill by $0.11. That did not mean the strategy had no value; it meant the old performance number had included a trade selection the live process could not reproduce.
One small nuisance: exchange symbol formats make expiration bugs look like data-quality bugs. We found a vendor’s weekly code that differed from the monthly code by one character. I still keep the raw symbol beside the normalized contract key in audit logs. It is not elegant, but it has saved us from trusting a tidy join too quickly.
If you are building an options backtest, save the full candidate set at every decision, along with each contract’s listing status, quote timestamps, sizes, and rejection reason. Then make contract selection a deterministic function of that snapshot. An expiration choice is part of the strategy. If the backtest can see a contract that paper could not, the signal comparison starts after the answer has already changed.
← All posts


