Backtest realism2026Published7 min read
Execution playbook — part 1 of 2
The backtest that lies, and the rules that stop it
A condensed playbook for honest short-horizon backtests: execution order inside one bar, reaction latency, slippage, payout bugs, timestamp alignment and leakage audits. They are the rules that kept my Polymarket research honest.
- Context
- Polymarket 15-minute and second-scale HFT research
- Applies to
- Any short-horizon or event-driven backtest
- Stress test
- 1 s latency · 2% slippage · min_ask_size 500
- Stack
- Python vectorized sweeps, optional Rust fill logic
- Backtesting
- Look-ahead bias
- Reaction latency
- Slippage
- Leakage checks
- Python
- Polymarket

- Problem
- A short-horizon backtest flatters you by default. It fills at quotes that are already gone, reads the whole bar with hindsight and assumes the book always has size for you.
- Approach
- Treat the simulator as the adversary: fix the event order inside every bar, charge latency, slippage and fees, build features on local receipt time and audit payouts and leakage directly.
- Outcome
- A 98% hit rate fell to ~55% once reaction latency reached 300 ms. With honest costs, expected value in the 15-minute markets stayed near zero, so I stopped that strategy on evidence.
Most inflated results come from three mistakes
Most inflated backtests I have seen came from one of three mistakes, and each one produces numbers that look technically respectable.
- Look-ahead inside the bar. The simulator enters and exits on information from the same bar, in whatever order suits it. That is time travel.
- Ghost quotes. The strategy trades the quote it saw at time
t. By the time a real order reaches the book, that quote is often gone. - Fantasy liquidity. A visible price is treated as an available fill, at any size.
I learned all three on Polymarket, first in 15-minute Up/Down markets and then in second-scale HFT research. None of the fixes is specific to prediction markets. They apply to any backtest where one bar of hindsight or a few hundred milliseconds can change the answer.
Look-ahead
Inside one bar: stop, then take-profit, then entry
A bar, or a block of snapshots, tells you which prices traded inside it, not in which order. Check everything against the bar's range at once and a position can fill at the low and exit at a high that came first. The fix is simple and boring: the same order inside every bar.
Stop
Pessimistic tie-breakCheck every open position against its stop first. When the stop and the take-profit both sit inside the bar's range, the stop wins: you cannot know which traded first, so assume the one that hurts.
Take-profit
Absolute levelsOnly positions that survived the stop check can take profit, at their own level. Levels are absolute prices (
entry_range,tp_range,stop_range), not magic deviations: a fixed price either traded in the bar or it did not.Entry
No same-bar round tripsNew entries go last. A position opened inside this bar meets its stop and take-profit from the next bar on, never prices that may have come before it.
How one bar books a trade that never happened
Range-based check
t2 entry fillst1 take-profit, earlier
The low reaches the entry and the high reaches the take-profit, so the bar books a winning round trip. The take-profit traded before the position existed.
Ordered check: stop → take-profit → entry
- 1 Stop·
- 2 Take-profit·
- 3 Entry
- next bar
Exits are checked before the entry exists. The new position carries into the next bar and has to reach its take-profit there.
Ghost quotes
Fill where the order lands, not where the quote was
A backtest that fills at the quote it sees at time t is trading a ghost. A real order needs reaction time to compute, decide and send, plus network time of 50–200 ms at minimum, often more. By the time it reaches the book, that ask may be gone.
The clearest case came from the HFT pipeline. Buying at the ask at t and selling at the bid five seconds later, with no reaction latency, the hit rate looked like ~98%. Acting at t + 300 ms instead, it dropped to around 55%, which is much more realistic.
I left the rule as a comment in the backtest code: "Quant audit: use ≥ 300 ms so you don't inflate hit rate with ghost quotes (98% → ~55% real)." Reaction latency (reaction_latency_ms) belongs in version one of a backtest, not in the apology after live trading disappoints.
If the edge only lives in the first row, it is probably not an edge
| Backtest setup | What it showed | How to read it |
|---|---|---|
| No latency: fill at the quote seen at t | ~98% hit rate | Flattering, not actionable |
| Reaction latency of 300 ms or more | ~55% hit rate | A more honest baseline |
| 300 ms + 1–2% slippage + liquidity filter | Often weak or negative | Where many fake edges disappear |
| Stress: 1 s latency, 2% slippage, min_ask_size 500 | PnL must not collapse | If it does, the model is not ready for live |
Hit rates from the HFT pipeline's 5-second taker backtest. The last two rows describe the typical outcome, not a single measured number.
Fantasy liquidity
A visible price is not an available fill
Add slippage, a minimum visible size (min_ask_size), queue effects or maker-fill uncertainty, and a strategy that looked clean can collapse very quickly. So every short-horizon backtest now gets 1–2% slippage and a liquidity filter from the start.
The label has to respect the same reality. In the HFT pipeline a sample only counts as a win if up_bid[t + h] > up_ask[t] + fee: buy at the ask, sell at the bid, pay the fee. Never at the mid.
Even that still overstates PnL. A bid on screen is not a fill: the market maker can cancel it, and confirmation can take 2–5 seconds.
Three bugs that made bad backtests look brilliant
The textbook warnings were not the whole list. My own repository surfaced three failure modes that inflated results while still looking technically respectable.
01
Never let the label pay out
A taker backtest once used the binary outcome as a fallback payout whenever the future exit price was missing. That fakes a clean $1 or $0 exit and inflates PnL immediately.
Rule: missing future prices are handled explicitly, never filled in with the answer.
02
Count each adverse event once
A maker simulator that re-evaluates every tick can count the same adverse event several times, unless re-evaluation carries a queue penalty or a cancel cooldown.
Rule: a queue penalty or cancel cooldown between re-evaluations.
03
Exclude the final minute when it misleads
The last minute of a 15-minute market is often trivial or structurally unrealistic, so including it creates fake signal. In one SOL market, two final-minute entries at 0.03, each closed at a 0.50 take-profit for +$15.67, carried almost all of a $32.11 paper profit.
Rule: exclude the final 60 seconds when that segment is structurally misleading.
Build features from what your machine had received
A second class of lies comes from data alignment, and it matters as much as fills.
In an asynchronous system, two feeds almost never update in the same millisecond. The honest join is often last known value on local receipt time: each row sees only what my machine had actually received, and feature windows follow the same clock instead of a future-friendly exchange timestamp.
For the HFT work I recorded my own data rather than relying only on downloaded history. A 24-hour recorder stored local receipt time (local_receipt_ts_ms, L) and the exchange event time (E) when available, so network_latency_ms = L − E became a column. Rows above a 200–500 ms latency threshold were dropped so the model did not train on lag spikes, freshness columns made the remaining staleness explicit, and an explicit leakage check ran on top.
A model can be mathematically correct and still be temporally dishonest.
Ten checks before I trust a short-horizon backtest
For short-horizon systems these are baseline conditions, not refinements. Anything less can still help with exploration, but not with confidence.
- Event order matches real execution: stop, then take-profit, then entry inside every bar, at absolute price levels.
- Temporal splits: train and test separated by market slug or time interval, never by shuffled rows.
- Reaction latency from version one: 300–1000 ms, never zero.
- Slippage: 1–2% on short horizons.
- Liquidity: a minimum visible size such as
min_ask_size = 500, instead of assuming the book always wants you. - Executable labels: buy at the ask, sell at the bid, pay the fee. Never the mid.
- No magical fallbacks: a missing future price is handled explicitly, never replaced by the outcome.
- Final 60 seconds excluded when that segment is structurally misleading.
- Receipt-time features: last known value on local receipt time, a 200–500 ms latency filter and an explicit leakage check.
- A stress run before any verdict: 1 s latency, 2% slippage,
min_ask_size = 500. If PnL collapses, the model is not ready for live.
An honest simulator is still not the market
A stricter backtest gets closer to reality. It does not replace paper trading and, with care, small live tests.
Modelled
- Event order inside every bar
- Reaction latency of 300–1000 ms
- Slippage, fees and a minimum visible size
- Local receipt time and latency filters
- Leakage checks and temporal splits
Still approximated
- Queue position, which stays simplified
- Round-trip timing, which is never exact
- Adverse selection, only through proxy labels such as target_adverse_bid_1s
- Maker fills: surviving in the book is not a fill without tape or explicit fill evidence
A simulator that never hurts you is usually flattering you.
Use a backtest to kill ideas, not to fall for them
The practical rule: use the backtest to disqualify ideas aggressively. If the numbers survive realistic pain and realistic timestamps, the strategy has earned more attention. If not, the simulator already did its job.
That rule kept the 15-minute system grounded. Once fills were sequenced and latency, fees and slippage were charged together, expected value stayed near zero after costs, and I stopped the strategy on evidence rather than hope. It also made the HFT work much harder to fake: good classifier metrics still had to survive the step to executable PnL, and the HFT write-up shows where they did not.
Honest simulation is half of the playbook. The other half is operational, keeping a live bot in sync with the exchange: why a bot is infrastructure, not edge.
Related
Execution research
Trading 15-minute Polymarket markets until the data said stop
A vectorized backtester, walk-forward sweeps and a live order-book bot for 15-minute Polymarket markets, stopped when expected value after fees stayed near zero.
Latency and infrastructure
HFT on Polymarket: Rust, timestamps and the 98% hit rate that wasn't
Second-scale Polymarket research built recorder-first, with Rust on the hot path and LightGBM on top, and a 98% hit rate that fell to about 55% once latency was simulated.

Market systems2025–2026Stopped
Polymarket trading systems
Prediction-market research: a dataset builder, a 15-minute bot and a Rust + Python HFT recorder.
Get in touch
Working on something similar? I am happy to compare notes.