To backtest a Polymarket strategy, pull an outcome token's price history from the Data API v2 /v2/prices-history endpoint, replay your rule bar by bar, fill each signal on the next bar after adding the spread and Polymarket's taker fee, and value whatever is left at the settlement payout. The official Python package, polymarket-client, wraps the endpoint as list_price_history(). A backtest tests your rules and your cost model; it does not predict profit.
Key points:
-
Data:
/v2/prices-historyfor prices,/v2/tradesfor individual trades. No API key is needed for either. - Limits: explicit time windows cap at 15 days; one-minute data is kept for at least 7 days, while 3-hour and 12-hour data is permanent (checked on 10 Oct 2026).
- Costs: the history has one price per bucket, so you add the spread yourself, plus the taker fee of C x feeRate x p x (1-p) on markets that charge one.
- Lookahead: act on the bar after the signal, and never trade the settlement point.
What You Can and Can't Backtest on a Prediction Market
You can backtest rules that trade on price moves, such as trend, mean reversion or fading extremes, because Polymarket publishes a price series for every outcome token. You cannot fully backtest anything that depends on queue position, order book depth or your own market impact, because the history does not record the book.
| Can backtest from public data | Cannot backtest reliably |
|---|---|
| Signals built on the price series of one outcome | Where a resting limit order would have sat in the queue |
| Trade counts, sizes and sides from the trades feed | Past order book depth and spreads (only the current book is served) |
| Taker fees, using the market's current fee settings | How your own orders would have moved the price |
| Holding to resolution, using the final payout | How an unclear resolution or a dispute will be decided |
Prediction markets also end. Each market resolves to a payout, so a strategy that holds to expiry is really a bet on the outcome, and one resolved market is a single sample, not a long track record.
Getting Historical Data from the Polymarket APIs
Polymarket serves historical prices from GET https://data-api.polymarket.com/v2/prices-history and historical trades from GET /v2/trades on the same host. Both are part of Data API v2, which needs no API key. Data API v1 is retired on 24 October 2026, so new code should start on the /v2 routes (Data API v2 overview, checked on 10 Oct 2026).
Uptime
99.999% uptime. Built for 24/7 trading reliability.
Your VPS runs in Tier IV datacenters with redundant power and network, monitored 24/7 on site.
See the uptime SLAFrom $29.99/mo · Windows or Linux
| Endpoint and SDK method | What it returns | Limits (checked on 10 Oct 2026) |
|---|---|---|
/v2/prices-history, list_price_history() |
Price points for one outcome token: timestamp, price, resolution_seconds, oldest first |
start/end windows up to 15 days; interval presets 1h, 6h, 1d, 1w, 1m, max, all; bucket_seconds 60 to 86,400; up to 10,000 points a page; 200 requests per 10 seconds |
/v2/trades, list_trades() |
Trades for a market, event or wallet: side, size, price, timestamp, outcome | Up to 1,000 rows a page; market and event feeds cover a fixed three-year window; taker side only by default; 300 requests per 10 seconds |
/v2/resolutions, get_resolutions() |
Resolution state and payouts for a market | Up to 20 condition IDs per request |
Fine detail does not last. Polymarket keeps one-minute data for at least 7 days, five-minute data for at least 60 days and thirty-minute data for at least 90 days; 3-hour and 12-hour data is permanent. interval="max" returns a market's whole life at 12-hour buckets by default. If you ask for a bucket size the store no longer holds, you get an empty or sparse page rather than a different resolution, so count your rows.
Install the SDK with pip install polymarket-client; Data API v2 support starts at version 0.10.0. To check a token by hand, the prices and order books page shows this request:
curl "https://data-api.polymarket.com/v2/prices-history?token_id=$TOKEN_ID&interval=1d&bucket_seconds=3600"
For trades, the SDK pages through /v2/trades for one market:
from polymarket import PublicClient
SLUG = "will-the-us-confirm-that-aliens-exist-before-2027-789-924-249"
with PublicClient() as client:
market = client.get_market(slug=SLUG)
pages = client.list_trades(condition_id=[market.condition_id], page_size=100)
trades = pages.first_page().items
for trade in trades[:5]:
print(trade.timestamp, trade.outcome, trade.side, trade.size, trade.price)
print(len(trades), "trades on the first page")
A Simple Python Backtest with One Example Strategy
The script below backtests one rule on hourly bars: buy 100 YES shares when the last closed bar is above its 24-bar average, and sell when it falls below. It uses the synchronous PublicClient, reads the fee rate from the market record and fills every signal on the following bar. Pass a market slug as the first argument, or run it as is to use the example market from Polymarket's docs.
import sys
from decimal import Decimal
from polymarket import PublicClient
SLUG = sys.argv[1] if len(sys.argv) > 1 else (
"will-the-us-confirm-that-aliens-exist-before-2027-789-924-249"
)
INTERVAL = "1m" # last month; use "max" for a market's whole life
BUCKET_SECONDS = 3600 # hourly bars
LOOKBACK = 24 # bars in the moving average
SHARES = Decimal("100") # shares per trade
HALF_SPREAD = Decimal("0.01") # assumed cost of crossing the spread, per share
def taker_fee(shares, price, rate):
# Polymarket's published formula, rounded to 5 decimal places
return round(shares * rate * price * (1 - price), 5)
with PublicClient() as client:
market = client.get_market(slug=SLUG)
yes = market.outcomes.yes
asset_id = yes.position_id if market.version == "v2" else yes.token_id
schedule = market.trading.fee_schedule if market.trading.fees_enabled else None
rate = schedule.rate if schedule else Decimal("0")
pages = client.list_price_history(
asset_id=asset_id, interval=INTERVAL, bucket_seconds=BUCKET_SECONDS
)
points = [point for page in pages for point in page.items]
# Bars only: the last point is an exact tick (or the settlement payout), not a bar
bars = [p for p in points if p.resolution_seconds > 0]
cash, shares, fees, trades = Decimal("0"), Decimal("0"), Decimal("0"), 0
for i in range(LOOKBACK, len(bars) - 1):
average = sum(b.price for b in bars[i - LOOKBACK:i]) / LOOKBACK
signal = bars[i].price # known once bar i has closed
next_price = bars[i + 1].price # we can only trade on the following bar
if shares == 0 and signal > average:
fill = min(next_price + HALF_SPREAD, Decimal("0.99"))
fee = taker_fee(SHARES, fill, rate)
cash -= SHARES * fill + fee
shares, fees, trades = SHARES, fees + fee, trades + 1
elif shares > 0 and signal < average:
fill = max(next_price - HALF_SPREAD, Decimal("0.01"))
fee = taker_fee(shares, fill, rate)
cash += shares * fill - fee
shares, fees, trades = Decimal("0"), fees + fee, trades + 1
final = points[-1].price if points else Decimal("0")
print(f"{market.slug}: {len(bars)} bars, fee rate {rate}")
print(f"trades {trades}, fees {fees:.2f}, open shares {shares}")
print(f"net result {cash + shares * final:.2f} USDC (open shares valued at {final})")
The rule only demonstrates the plumbing; it is not a strategy we recommend. Swap in your own rule, then test it across many markets, not one.
Two details are worth copying. The YES identifier depends on the market version (position_id for v2 markets, token_id otherwise), as the market details docs show. And the paginator is read to the end, because the final point of the series sits on the last page.
Each signal is filled on the bar after it, never on the bar that produced it.
Modelling Fills, the Spread and Polymarket Fees
A realistic fill model buys at the ask and sells at the bid, then subtracts Polymarket's taker fee. The price history gives one price per bucket, not a bid and an ask, so the script adds HALF_SPREAD to buys and takes it off sells.
Locations
Trade beside your market. 8 datacenters, 3 continents.
Pick the location closest to what you trade, not to where you live.
Compare locationsFrom $29.99/mo · Windows or Linux
To pick a sensible spread, look at the live book: the SDK's get_spread() and get_order_book() return today's spread and price levels for a token. That is only today's book, so widen the assumption for thin or fast markets. Our guide to the Polymarket order book explains bids, asks and matching.
Polymarket's fees page gives the taker fee as:
fee = C × feeRate × p × (1 - p)
C is the number of shares and p is the price. Makers are never charged fees, and fees are rounded to 5 decimal places. The rate depends on the market category:
| Category | Taker fee rate | Fee on 100 shares at 50c |
|---|---|---|
| Crypto | 0.07 | $1.75 |
| Sports, Economics, Culture, Weather, Other | 0.05 | $1.25 |
| Finance, Politics, Mentions, Tech | 0.04 | $1.00 |
| Geopolitics | 0 (fee-free) | $0 |
Rates are from Polymarket's fees page, checked on 10 Oct 2026. Read each market's own settings from market.trading.fees_enabled and market.trading.fee_schedule.rate, as the script does. Those are today's settings; a backtest over older data assumes the same fee applied then.
Resolution Risk and Lookahead Traps
The two biggest ways a Polymarket backtest flatters itself are using information before it existed and ignoring how the market resolved. Both are easy to build in by accident.
-
Bucket timestamps. An aggregate point reports its bucket start, but the price was observed somewhere inside
[timestamp, timestamp + resolution_seconds). Treat a bar as known only after it closes, and fill on the next one. -
The settlement point. A resolved market's series ends with the on-chain payout (0 lost, 1 won, fractional for voided or split results) at
resolution_seconds0. It is not a price anyone could trade, so the script drops it from the bars. - Picking markets by outcome. Choosing only markets that resolved YES, or only ones you remember, bakes the answer into the test. Pick markets by rules you could have applied at the time.
- Resolution rules. Polymarket says to read the rules before trading: the title describes the question, but the rules define how it resolves. A disputed proposal goes to further rounds or a vote, which a price series cannot show.
If you hold positions to expiry, check the outcome with get_resolutions() and use only payouts from a row whose status is resolved. Positions in a market that has not settled are still open risk, which is why the script values them at the last price and says so.
From Backtest to a Live Bot on a VPS in Dublin
Once a rule survives backtesting across many markets, the next step is paper trading it on live data, then a small live version. Our Polymarket trading bot guide covers the SDK setup, a paper ledger, wallet authentication and deployment checks.
Check eligibility before you trade live. Polymarket's geographic restrictions page lists the United States as close-only on both the frontend and the API: users there can close existing positions but cannot open new ones.
A live bot has to keep running through the night and through reconnects. QuantVPS hosts its Polymarket VPS in Dublin, with always-on DDoS protection with advanced filtering. QuantVPS servers run with 99.999% uptime. Performance plans run on the AMD Ryzen 9 9950X, which also helps when you rerun backtests across hundreds of markets.
Order flow
Built for scalpers. Heatmaps on dedicated resources.
Order-flow heatmaps and DOM ladders run on dedicated CPU, memory and NVMe, so your charts keep responding during volatility.
See the dedicated hardwareFrom $29.99/mo · Windows or Linux
FAQs
Does Polymarket have an API for historical prices?
Yes. Data API v2 serves GET /v2/prices-history for one outcome token at a time, with no API key. In Python, call list_price_history() on a PublicClient or AsyncPublicClient from the polymarket-client package.
How far back does Polymarket price history go?
For the oldest markets, back to 2022. Three-hour and twelve-hour data is permanent, while one-minute data is kept for at least 7 days, five-minute for at least 60 and thirty-minute for at least 90 (checked on 10 Oct 2026).
Can I backtest limit orders on Polymarket?
Only roughly. The public history has no past order book or queue data, so you cannot know whether a resting order would have filled. Model limit orders as filled only when the price trades through your level, and treat the result as optimistic.
Do I need to include fees in a Polymarket backtest?
Yes, for any market where fees are enabled. Takers pay C x feeRate x p x (1-p), with rates from 0.04 to 0.07 by category; makers pay no fees, and Geopolitics markets are fee-free (checked on 10 Oct 2026).









