Execution backtesting on Polymarket: order level replay
A backtest worth trusting has to answer two questions: would the signal have made money, and would your orders actually have filled? This guide covers the execution engine, which answers both by filling your orders against recorded L2 books. For testing signals over long windows and many markets at once, see the alpha backtesting guide.
Step 1: pick a series
Pick any series from the data catalog and note its slug; sports series take subtype= to pick one bet type. Install the SDK and set MARKETLENS_API_KEY.
$ pip install marketlensStep 2: write a Strategy subclass
A strategy is a class with hooks the engine calls during replay; this one implements all six. When the spread is tight and the book is bid heavy, it buys YES once per market.
from marketlens import MarketLens
from marketlens.backtest import Strategy
class ImbalanceBuyer(Strategy):
def on_market_start(self, ctx, market, book):
self._entered = False # runs once per market
def on_book(self, ctx, market, book):
if self._entered:
return
if book.spread < 0.02 and book.imbalance(3) > 0.3:
ctx.buy_yes(size=100, limit_price=book.best_ask)
self._entered = True
def on_trade(self, ctx, market, book, trade):
pass # every historical trade
def on_fill(self, ctx, market, fill):
ctx.cancel_all() # our order filled
def on_reject(self, ctx, market, order):
self._entered = False # rejected: allow a retry
def on_market_end(self, ctx, market):
print(market.question, ctx.position().shares)The other actions and state available inside hooks:
ctx.buy_yes(size, limit_price=..., cancel_after=...)
ctx.buy_no(...); ctx.sell_yes(...); ctx.sell_no(...)
ctx.cancel(order); ctx.cancel_all()
ctx.position(), ctx.cash, ctx.equity, ctx.bookThe full reference is in the backtesting docs.
Step 3: run the backtest
One call runs it. The target is a series slug, a market UUID, or a list of either; after and before bound the window. Start small: runtime grows with the data in the window.
client = MarketLens()
result = client.backtest(
ImbalanceBuyer(),
"btc-up-or-down-5m",
initial_cash=10_000,
after="2026-04-15T00:00:00Z",
before="2026-04-15T06:00:00Z",
)
print(result.summary())Orders interact with the recorded book: a market order consumes real resting depth, and a limit order waits at its price until recorded trades would have reached it. The fill you get is the fill the book would have given you.
Step 4: iterate
Change one threshold, rerun the same window, compare. When a variant looks good, widen the window and try a different week or series before believing it. Everything the result object offers, metrics, DataFrames, and the dashboard, is covered in the results guide.
Step 5: turn up the realism
The knobs stress the assumptions your strategy depends on.latency_ms delays every order. slippage_bps penalizes market order fills on top of real depth consumption.queue_position simulates the CLOB queue for limit orders: fills only happen once the size ahead of you drains, which is only possible because the data records every change at your price level. limit_fill_rate and settlement_delay_ms round out the set.
result = client.backtest(
ImbalanceBuyer(),
"btc-up-or-down-5m",
initial_cash=10_000,
after="2026-04-15T00:00:00Z",
before="2026-04-15T06:00:00Z",
latency_ms=200,
slippage_bps=10,
queue_position=True,
fees="polymarket",
)An edge that survives higher latency, added slippage, and queue modeling is a different animal from one that only profits at the defaults. Run the sweep before trusting any number.
Settlement: winning outcome, not final price
Positions held to the end settle by the recorded winning outcome, not the last traded price. The two usually agree but not always: a market can trade near one side into the close and resolve the other way. The engine pays winning shares $1 and losing shares $0.
Iterate offline with data_dir
Pass data_dir= to any backtest: the first run downloads the window as Parquet files, and every later run replays from disk with no API events spent.
data = client.exports.download_series(
"btc-up-or-down-5m",
after="2026-04-15", before="2026-04-16")
result = client.backtest(
ImbalanceBuyer(), "btc-up-or-down-5m",
data_dir=data, initial_cash=10_000,
after="2026-04-15", before="2026-04-16",
)
# edit the strategy, run again: replays from diskFAQ
Common questions
How do I backtest a Polymarket strategy?
Install the Marketlens Python SDK, subclass Strategy with hooks like on_book and on_fill, and run the backtest call with a series slug, starting cash, and a time window. The engine replays recorded L2 order books tick by tick and fills your simulated orders against real historical depth.
Does the backtest account for slippage and queue position?
Yes, both are configurable. Market orders fill against recorded book depth with optional extra slippage, limit orders can be simulated with CLOB queue position so they only fill once the queue ahead of you is drained by real trades, and every order can carry latency. The Polymarket fee schedule is applied by default.
How are markets settled in a Polymarket backtest?
By the recorded winning outcome, not by the last traded price. A small fraction of markets end with the tape on the losing side of the eventual resolution, so settling positions by final price would misstate P&L. Positions held to resolution pay $1 per winning share and $0 per losing share.
Can I run Polymarket backtests offline without spending API quota?
Yes. Point the backtest at a local data directory: the first run downloads the window as Parquet files and every later run replays entirely from disk. Bulk exports feed the same directory for explicit prefetching.
Try it
Run your first backtest today
The free tier includes 5M events per day with full API and full archive access, no card required.
$ pip install marketlens