How to Backtest a Polymarket Trading Bot for Slippage
Most Polymarket backtests fill orders at the best ask or midpoint. Here's how to build one that replays real order-book depth instead.

Your backtest just told you a strategy nets a healthy edge on Polymarket YES tokens. Then you deploy it live, and the P&L looks nothing like the equity curve you tested. The gap usually isn't a bug in your signal. It's a bug in your backtest, because it probably filled every order at the best ask or the midpoint, which is not how a central limit order book actually behaves once your size shows up.
Why do naive Polymarket backtests overstate performance?
A naive backtest assumes your order executes at a single reference price, usually the best ask for buys or the best bid for sells, sometimes the midpoint between them. That assumption holds only when your order size is small relative to the liquidity sitting at the top of the book. On a Polymarket CLOB market with thin depth, a real order of any meaningful size walks through several price levels, and your average fill price drifts away from that clean reference point.
The effect compounds across every trade in the backtest. If your strategy fires dozens or hundreds of trades over a test window, and each one is quietly priced a cent or two better than what you could actually get, the strategy's reported edge is systematically inflated. You end up validating a version of your bot that doesn't exist, because it trades in a market with infinite liquidity at the top price.
This matters more on Polymarket than on many traditional markets because prices are implied probabilities bounded between 0 and 1. A 0.02 slippage cost on a 0.50 price isn't a rounding error, it's a meaningful chunk of the spread between your model's fair value and the market's price. Strategies that look profitable on paper by half a cent of edge can flip negative once realistic execution costs are applied.
What does a realistic backtest actually need to simulate?
A realistic backtest needs to replay the actual order-book state at the moment each simulated trade would have occurred, not just a single price point. That means storing historical bid and ask levels with their sizes, then walking through those levels the same way a live order would consume them. It also means respecting incomplete fills: if the visible depth at that historical moment couldn't have filled your intended size, the backtest should record a partial fill or reject the trade entirely, not silently assume the rest filled at a fair price.
The core requirement is architectural, not statistical. Your backtest engine should call the exact same execution-simulation function that your live bot uses to estimate fills. If you maintain two separate code paths, one for backtesting and one for production, they will drift apart over time and your validation will quietly stop meaning anything.
How do you replay historical order-book depth in a backtest engine?
Start by storing snapshots as a time-indexed structure, each one holding a full ladder of bids and asks with price and size, plus a timestamp. During replay, your event loop advances through these snapshots in chronological order and, whenever your strategy generates a signal, looks up the most recent snapshot at or before that timestamp:
from decimal import Decimal
class OrderBookHistory:
def __init__(self, snapshots):
# snapshots: list of dicts sorted by timestamp
self.snapshots = snapshots For more on this, see [read about node --build-sea not working? fix it on node 24 lts](/node-build-sea-not-working-fix-it-on-node-24-lts).
def snapshot_at(self, timestamp):
candidates = [s for s in self.snapshots if s["timestamp"] <= timestamp]
if not candidates:
return None
return candidates[-1]
In practice you would index this with a binary search or a pointer that only moves forward, since scanning the full list on every lookup gets slow once you're replaying thousands of signals across weeks of tick data. The important design choice is that the backtest never peeks at a snapshot that occurs after the signal timestamp. That kind of lookahead bias is easy to introduce by accident and it silently inflates results the same way naive fills do.
How do you plug the same slippage logic into the backtest as live trading?
Reuse the identical fill-simulation function your live bot calls before submitting an order. If your production code has a simulate_buy_order function that walks the ask ladder and returns filled size, average price, and a completeness flag, your backtest should import that exact function rather than reimplementing a shortcut version:
def backtest_trade(book_history, signal, max_slippage):
snapshot = book_history.snapshot_at(signal["timestamp"])
if snapshot is None:
return {"status": "no_data"}
result = simulate_buy_order(
asks=snapshot["asks"],
target_size=signal["size"],
)
max_price = signal["reference_price"] + max_slippage
if not result["complete"] or result["average_price"] > max_price:
return {"status": "rejected", "result": result} For more on this, see [a closer look at byzantine quorum size formula: why 3-of-4 beats 2-of-3](/byzantine-quorum-size-formula-why-3-of-4-beats-2-of-3).
return {"status": "filled", "result": result}
This single change, sharing the execution function between backtest and live code, is what turns a backtest from a rough sketch into an actual validation step. If a trade would have been rejected for insufficient depth or excess slippage in production, it should be rejected in the backtest too, and that rejection should show up in your reported trade count.
How should you report naive versus realistic P&L?
Run two ledgers in parallel during the same replay. The naive ledger prices every fill at the best ask or midpoint from the snapshot, ignoring depth. The realistic ledger uses the output of simulate_buy_order against the same snapshot and signal. Track both equity curves, plus a running total of the dollar difference between them, so you can see exactly how much of your naive edge slippage consumed.
Also read: why benchmark results vary after reboot: thermal throttling — background
It also helps to report a rejection count separately from the P&L numbers. A strategy that looks fine on filled trades but rejects half its signals for insufficient liquidity has a very different risk profile than one that fills almost everything. That rejection rate is information your naive backtest can't produce at all, because it never checks depth in the first place.
Where do you get historical order-book data for this kind of replay?
You need full depth snapshots, not just trade prints. Polymarket's CLOB exposes bid and ask levels along with tick size, minimum order size, a timestamp, and a book hash for state reconciliation. To backtest properly, you need to capture and store that ladder over time, either by polling the book at short intervals or by recording a websocket feed of book updates.
Trade-price history alone won't tell you what depth existed a level or two beyond the best price at any given moment, which means it can't answer the slippage question at all. If you're only logging last-trade prices today, that's the first gap to close before any slippage backtest will be trustworthy.
Doesn't backtesting on midpoint prices already account for the spread, so slippage is covered?
This is the most common misconception, and it's worth being direct about why it's wrong. Midpoint pricing accounts for the gap between best bid and best ask, but it says nothing about what happens once your order size exceeds what's sitting at that top level. Recall the earlier example: with asks of 100 shares at 0.50, 150 at 0.51, and 300 at 0.53, a 300-share buy order produces an average fill price near 0.5117, not the best ask of 0.50 and nowhere near a tidy midpoint calculation either.
A midpoint-based backtest might look more conservative than a best-ask backtest, but it's still a single-price approximation that ignores size entirely. The only way to capture the real cost is to walk the ladder for the exact size your strategy intends to trade, at the exact historical moment it would have traded. Anything less is measuring a bot that trades in a market that doesn't exist, and deploying capital against that measurement is how a profitable-looking backtest turns into a disappointing live account.
Related Articles

How to Use Claude Code Subagents to Parallelize Development
Learn how to enhance your development workflow using Claude Code Subagents. This guide provides practical examples for parallelizing coding tasks.
Sep 13, 2025

Unlocking ChatGPT Developer Mode: Full MCP Client Access
Unlock the power of ChatGPT Developer Mode with full MCP client access. Discover how to enhance your coding projects and streamline development.
Sep 11, 2025

WebAssembly: Unleashing Native Speed in Web Browsers
WebAssembly is transforming web development with near-native performance, enabling more complex and efficient applications.
Sep 6, 2025