How to backtest Polymarket strategies
Backtest a Polymarket strategy on tick-level order books with pm-hftbacktest, with realistic fills, queue position, latency and fees.
Why price history is not enough
Polymarket's API returns the current order book and a price line for each outcome token, not past order books. A backtest on the price line assumes every order fills at that price, in any size, the moment you decide to trade. On Polymarket that is rarely true: books are thin, prices move in one-cent ticks, and a 5-minute crypto market can cross most of its price range before it resolves.
A realistic backtest needs three things the price line does not have:
- The full order book over time, so the backtester knows what was available at each price level.
- Every change to it, so a resting limit order can move up the queue and fill only when the volume ahead of it has traded.
- Timestamps for when each update was received, so the backtest can model the delay between seeing a price and your order reaching the exchange.
PMData records every book and price_change message from Polymarket's WebSocket with both timestamps. pm-hftbacktest replays those files and simulates your orders against them.
1. Install pm-hftbacktest
pm-hftbacktest is a Polymarket fork of hftbacktest. It adds a Polymarket asset type, a converter for PMData files, and a fee model.
pip install pm-hftbacktest pandas
2. Get tick-level order book data
Each PMData L2 file is one market. This loads one BTC Up/Down 5m market; a free PMData account includes 600 downloads:
import numpy as np
import pandas as pd
from numba import njit
from hftbacktest import (
GTC,
LIMIT,
BacktestAssetPoly,
ROIVectorMarketDepthBacktest,
Recorder,
init_orderbook,
polymarket_to_hbt,
)
from hftbacktest.stats import PolyAssetRecord
# One market's L2 file. Get a free API key at https://pmdata.dev
slug = "btc-updown-5m-1788264000"
df = pd.read_parquet(
f"https://api.pmdata.dev/download/poly_l2/{slug}.parquet",
storage_options={"api_key": "<YOUR_API_KEY>", "User-Agent": "Mozilla/5.0"},
)
market_start_ns = int(slug.rsplit("-", 1)[1]) * 1_000_000_000
This endpoint covers crypto Up/Down 5m, 15m and 1h markets. The API reference lists the endpoints for every other market.
3. Convert it for the backtester
polymarket_to_hbt turns the L2 file into pm-hftbacktest's event format: book snapshots, depth updates, and both timestamps for each event.
# Convert it to pm-hftbacktest's event format.
data = polymarket_to_hbt(df)
4. Write the strategy
Strategies are plain Python functions compiled with Numba. The loop steps through time with hbt.elapse, reads the book with hbt.depth, and places orders. This one waits for a side to reach 0.88 after the window opens and joins it with a limit order:
# Strategy: once either side trades at 0.88 or more after the window opens,
# join that side with a limit order at 0.88 and hold to resolution.
# The book is the Up token: selling Up at 0.12 is the same bet as buying Down at 0.88.
@njit
def endgame(hbt, recorder, market_start_ns, trigger, qty):
if not init_orderbook(hbt, 0):
return
submitted = False
while hbt.elapse(100_000_000) == 0: # step 100 ms
if hbt.current_timestamp < market_start_ns:
continue
hbt.clear_inactive_orders(0)
depth = hbt.depth(0)
mid = (depth.best_bid + depth.best_ask) / 2.0
if not submitted and mid >= trigger:
hbt.submit_buy_order(0, 1, trigger, qty, GTC, LIMIT, False)
submitted = True
elif not submitted and mid <= 1.0 - trigger:
hbt.submit_sell_order(0, 1, 1.0 - trigger, qty, GTC, LIMIT, False)
submitted = True
recorder.record(hbt)
5. Run it with fees
The example uses the fee model from the pm-hftbacktest examples: a taker fee of 0.07 × p × (1 − p) and a maker rebate of 20% of it.
# Run it with Polymarket's fee: taker 0.07 * p * (1 - p), maker rebate 20% of that.
taker_fee = 0.07 * 0.88 * (1 - 0.88)
asset = BacktestAssetPoly().data(data).trading_value_fee_model(-taker_fee * 0.2, taker_fee)
hbt = ROIVectorMarketDepthBacktest([asset])
recorder = Recorder(hbt.num_assets, 5_000_000)
endgame(hbt, recorder.recorder, market_start_ns, 0.88, 5.0)
hbt.close()
Your order rests in the book behind the volume that was already at 0.88. It fills only when trades work through that queue, after the order latency the backtester applies, which is the difference between this and a price-line backtest.
6. Read the results
# Results: fills, position, and P&L at resolution.
stats = PolyAssetRecord(recorder.get(0)).stats(book_size=100)
stats.summary()
print("earn:", stats.earn)
On this market the price fell to Down, the strategy sold Up at 0.12, and the market resolved Down: a profit of about 0.60 on 5 shares after fees.
7. Test across many markets
One market proves nothing. Run the strategy over a full day or more, one market at a time:
from datetime import datetime, timedelta, timezone
def load_l2(slug):
return pd.read_parquet(
f"https://api.pmdata.dev/download/poly_l2/{slug}.parquet",
storage_options={"api_key": "<YOUR_API_KEY>", "User-Agent": "Mozilla/5.0"},
)
# All 288 btc-updown-5m markets of one UTC day, one every 5 minutes.
day = datetime(2026, 8, 8, tzinfo=timezone.utc)
slugs = [f"btc-updown-5m-{int((day + timedelta(minutes=5 * i)).timestamp())}" for i in range(288)]
trigger, qty = 0.88, 5.0
taker_fee = 0.07 * trigger * (1 - trigger)
results = []
for slug in slugs:
asset = BacktestAssetPoly().data(polymarket_to_hbt(load_l2(slug))).trading_value_fee_model(-taker_fee * 0.2, taker_fee)
hbt = ROIVectorMarketDepthBacktest([asset])
recorder = Recorder(hbt.num_assets, 5_000_000)
endgame(hbt, recorder.recorder, int(slug.rsplit("-", 1)[1]) * 1_000_000_000, trigger, qty)
hbt.close()
earn = PolyAssetRecord(recorder.get(0)).stats(book_size=100).earn
results.append({"market_slug": slug, "earn": earn})
pnl = pd.DataFrame(results)
print(pnl["earn"].describe())
print("total:", pnl["earn"].sum())
A strategy like this wins small amounts often and loses most of its stake when a market reverses after the trigger, so judge it on the total over many markets, not on single wins.
What makes a Polymarket backtest realistic
- Tick-level books, not snapshots. Snapshots every second or minute miss the changes in between, so queue position and short-lived liquidity are lost.
- Latency. Model the time from a book update to your order arriving. PMData's receive timestamps are collected in AWS London, colocated with Polymarket, at 4.6 ms median latency.
- Fees. Include taker fees and maker rebates; at 0.88 the fee is a large share of the edge.
- The real resolution price. 5m and 15m crypto Up/Down markets resolve on Chainlink TWAP and 1h markets on Binance candles; check outcomes against the right source. See the Up/Down series.
How close is the backtest to live trading?
We compared a live limit-order strategy with pm-hftbacktest on PMData data across 547 BTC 5-minute markets. The backtest matched live outcomes in 95.32% of markets and overstated live profit by 10.45%. Most of the gap came from execution latency: some fills that looked available in the simulation were gone by the time a live order arrived, so model latency conservatively.
Frequently asked questions
Can I backtest Polymarket with price history alone?
Only roughly. Polymarket's /prices-history endpoint gives a price line, not the order book, so a backtest on it assumes you trade at that price in any size. That ignores the spread, the depth at each level, your place in the queue, and the delay between seeing a price and your order arriving.
What data do I need for a realistic Polymarket backtest?
Tick-level L2 order book data: every book snapshot and price_change message, with Polymarket's timestamp and a receive timestamp. That lets a backtester replay the book as it was and decide whether your limit order would have filled.
Which backtesting tool works with Polymarket data?
pm-hftbacktest, a Polymarket fork of hftbacktest (pip install pm-hftbacktest). It reads PMData L2 files directly through polymarket_to_hbt, models queue position and order latency, and includes a Polymarket fee model.
How accurate is a tick-level Polymarket backtest?
In PMData's study of 547 BTC 5-minute markets, pm-hftbacktest on PMData data matched live trading outcomes in 95.32% of markets, while overstating live profit by 10.45%.
Is there free Polymarket order book data to try this on?
Yes. One BTC Up/Down 5m market is available without an API key at https://api.pmdata.dev/examples/polymarket/l2, and a free PMData account includes 600 downloads.
