All articles
Polymarket·May 15, 2026·6 min read

Backtesting Polymarket Strategies with Historical CLOB Data

A hands-on guide to Polymarket backtesting historical data: reconstruct CLOB order books from the subgraph and build a fill-aware backtester that survives live trading.

Most Polymarket backtests lie to you, and they lie in the same direction: they assume you got filled at the mid price on infinite size, the moment your signal fired. Run that backtest on a spread strategy and it prints money. Deploy it and you watch the book move away from every order you post while the fills you did get are exactly the ones you didn't want. The gap between those two outcomes is entirely about how honestly you reconstruct the order book and model your own fills.

This is the hard part of prediction-market strategy work, and it's the part nobody's Substack shows you. So let's build it properly.

What data actually exists, and what it isn't

Polymarket runs a central limit order book (CLOB) off-chain, with settlement on-chain via the CTF exchange contract. That split matters for backtesting because your data comes from two very different places:

  • The Data API (data-api.polymarket.com) gives you trade prints, market metadata, and current book snapshots. Trades are timestamped, sized, and tagged with the taker side. This is your ground truth for what executed.
  • The Goldsky subgraph indexes on-chain fills from the exchange contract — OrderFilled events with maker/taker asset amounts. Useful for cross-checking volume and for markets where you want the settled record rather than the API's view.

Neither gives you a full historical book replay. Polymarket does not hand you every resting-order add/cancel with microsecond stamps the way a crypto exchange's L3 feed would. There is no free lunch of a complete limit-order lifecycle stream. What you have is trades plus periodic book snapshots, and you reconstruct the rest.

If you're fuzzy on how the book and order lifecycle work under the hood, the mechanics of the CLOB order signing and matching flow are worth reading before you trust any reconstruction — the asset-in/asset-out accounting is easy to get backwards, and a sign error there quietly corrupts every fill in your sim.

Reconstructing book state between snapshots

Here's the practical approach that survives contact with reality. Pull book snapshots at whatever cadence you can get (the API's book endpoint, sampled), then interpolate the book between snapshots using the trade stream as evidence.

The logic is: a snapshot at t0 gives you resting depth. Every trade between t0 and the next snapshot t1 consumes depth on one side. You walk the trades forward, decrementing the levels they hit, so at any timestamp you have a maximum-consistent book — the resting liquidity that must have been there for those trades to print at those prices.

def book_at(snap, trades, ts):
    # snap: {'bids': [(px, sz)], 'asks': [(px, sz)]} at snap['ts']
    bids = {px: sz for px, sz in snap['bids']}
    asks = {px: sz for px, sz in snap['asks']}
    for tr in trades:
        if not (snap['ts'] <= tr['ts'] <= ts):
            continue
        side = asks if tr['taker_side'] == 'BUY' else bids
        remaining = tr['size']
        for px in sorted(side, reverse=(side is bids)):
            if remaining <= 0:
                break
            take = min(side[px], remaining)
            side[px] -= take
            remaining -= take
    return _clean(bids), _clean(asks)

This is deliberately conservative. It undercounts liquidity, because resting orders can be added and cancelled between snapshots and you never see those. That bias is good for a backtester — a strategy that survives on undercounted depth will survive on real depth. The failure mode you're avoiding is the opposite: assuming phantom liquidity that was never there.

Polymarket prices live in [0,1] and the two outcome tokens sum to 1, so always reconstruct both token books and check best_bid_YES + best_ask_NO ≈ 1. When that identity breaks by more than a tick, your snapshot and trade streams have drifted out of sync and the window is garbage. Drop it rather than trust it.

Modeling fills like you'll actually get them

A maker strategy and a taker strategy need different fill models, and mixing them up is the single most common way a Polymarket backtest overstates returns.

Taker fills are the easy case. You cross the spread, you walk the book you reconstructed, you pay the price of each level you consume plus the exchange fee. Model partial fills honestly: if you want 800 shares and the ask has 300 at 0.62 and 200 at 0.63, you get 500 shares at a blended 0.624 and the rest goes unfilled or chases.

Maker fills are where discipline pays off. You post a resting order and you only get filled when a taker trades through your price. So your fill condition is: a trade printed on the opposite side at a price at or beyond your quote, and there was enough taker size left after the queue ahead of you. You have to model queue position, because on a market with 40k shares resting at 0.55, your fresh 500 sit at the back and most 500-share prints won't reach you.

A rough but honest queue model:

def maker_fill(my_px, my_size, resting_ahead, prints):
    filled = 0
    for tr in prints:                 # opposite-side taker prints
        if not crosses(tr['price'], my_px, my_side):
            continue
        depth = tr['size'] - resting_ahead
        resting_ahead = max(0, resting_ahead - tr['size'])
        if depth > 0:
            got = min(depth, my_size - filled)
            filled += got
        if filled >= my_size:
            break
    return filled

Set resting_ahead from the reconstructed depth at your price level when you posted. It's approximate — you don't know true queue order — but it kills the fantasy of instant maker fills. This queue-aware honesty is the same thing that separates a profitable Polymarket market-making bot from one that bleeds; the full market-making walkthrough goes deeper on inventory skew and requoting.

The gotchas that eat backtests

A few things that will silently ruin your numbers:

  • Resolution and settlement. A market resolving to YES pays 1.00, but the path to resolution matters for anything holding inventory. If your strategy carries positions into the UMA dispute window, model that you can't exit at fair value near resolution. How the optimistic oracle actually resolves markets determines when liquidity dries up, and it dries up early.
  • Fee changes. Polymarket's fee schedule has changed over time. Hardcoding today's fee onto 2023 trade data flatters or punishes the wrong strategies. Stamp fees by date.
  • Look-ahead via snapshots. If you use a snapshot at t1 to decide a trade at t0.5, you've cheated. Only ever query book_at with data strictly at or before your decision timestamp.
  • Survivorship in market selection. Backtesting only on markets that ended up liquid is a classic trap. Include the dead ones; a spread strategy's real drawdown lives in markets that stopped trading.

A sane validation loop

Once your sim runs, don't trust the PnL curve. Trust the fill log. Sample 30 fills, replay the exact book state at each, and hand-check that the fill was achievable. If your backtester claims a maker fill where no opposite-side print crossed your price, you have a bug, and it's the expensive kind.

For arbitrage-style strategies the reconstruction has to be tighter still, because cross-market Polymarket arbitrage lives on 1–2 cent edges that vanish the instant your fill model gets optimistic. A spread bot has more margin for reconstruction error; a two-legged arb has almost none, and a backtest that gets fills slightly wrong will show phantom edge on every trade.

Build the fill model first, the signal second. A mediocre signal on an honest backtester beats a brilliant one on a fantasy fill engine every time — because only one of them tells you the truth before your capital does. If you'd rather start from a reconstruction pipeline that's already been through live fills, that's exactly what we build into every Polymarket spread bot we ship.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Polymarket#backtesting#CLOB#prediction markets#quant