> Documentation index: https://pmdata.dev/llms.txt. Full docs in one file: https://pmdata.dev/llms-full.txt

# How to backtest Polymarket strategies

Source: https://pmdata.dev/docs/knowledge-base/how-to-backtest-polymarket

> 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](https://github.com/mileswangs/pm-hftbacktest) is a Polymarket fork of hftbacktest. It adds a Polymarket asset type, a converter for PMData files, and a fee model.

```bash
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](https://pmdata.dev/polymarket-crypto-data/btc-updown-5m) market; a free PMData account includes 600 downloads:

```python
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](https://pmdata.dev/docs/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.

```python
# 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:

```python
# 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.

```python
# 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

```python
# 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:

```python
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](https://pmdata.dev/polymarket-crypto-data#updown-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](https://pmdata.dev/research/polymarket-backtest-accuracy-95-32) 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.
