Recipe 25 of 30
Backtest a Polymarket strategy on the real historical book (fills, fees, lookahead)
Take a day of recorded top-of-book or full-book quotes from the archive and the same day's on-chain outcomes from /settlements/bulk, decide only on quotes received before each decision, fill at the standing price up to its size, and add fees yourself.
A backtest needs two things the API keeps apart: the book as you could have seen it, and how each market ended. GET /v1/polymarket/settlements/bulk gives the second in one call per day: each market's winning token, from the chain. The archive holds the first, one file per stream and UTC day (l1_quotes for the touch, book_events for full depth). Join them in DuckDB and price every entry against the quote that stood at the moment you would have decided. The bulk parameters are on Settlements in bulk.
Quick answer
Fetch how every market of the family resolved on the day you test.
# how every 5-minute BTC market resolved on 2026-10-23: the payoff side of the backtest
curl -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/v1/polymarket/settlements/bulk?slug_prefix=btc-updown-5m-&from_ms=1792713600000&to_ms=1792800000000&limit=5000"Example response
Captured from the live API on 2026-10-24 at 17:22 UTC, with limit=3, trimmed to 2 rows; at limit=5000 the same rows open the page. A winning token pays $1 a share and the other pays 0. The slug carries the window's start in unix seconds, so the window closes 300 seconds later; resolved_ts_ms is when the chain settled it, 81 and 143 seconds after the close for these two.
{
"settlements": [
{
"condition_id": "0xe8eb153c68bb090d0f7378220dda294de0fb903ca7455298e889115bd8db3926",
"market_known": true,
"outcomes": ["Up", "Down"],
"payouts": [1, 0],
"question": "Bitcoin Up or Down - October 22, 7:55PM-8:00PM ET",
"resolved_block": 96005540,
"resolved_ts_ms": 1792713681000,
"slug": "btc-updown-5m-1792713300",
"token_ids": ["66807904852803027944717699017367321508323078567048144477724810240924212801296", "31737787044827274982239313079132196514193684856716359909945275491390424775584"],
"winning_index": 0,
"winning_outcome": "Up",
"winning_token_id": "66807904852803027944717699017367321508323078567048144477724810240924212801296"
},
{
"condition_id": "0xc0b564b0d97b8494ea9c05312043825e03355064cf6c86dcd2857d02d6172496",
"market_known": true,
"outcomes": ["Up", "Down"],
"payouts": [0, 1],
"question": "Bitcoin Up or Down - October 22, 8:00PM-8:05PM ET",
"resolved_block": 96005781,
"resolved_ts_ms": 1792714043000,
"slug": "btc-updown-5m-1792713600",
"token_ids": ["95675224515755543774285417772928531590582853511339802875009054381294430813145", "71942067128320219824750689771945336085097224938258510367609625645394607749800"],
"winning_index": 1,
"winning_outcome": "Down",
"winning_token_id": "71942067128320219824750689771945336085097224938258510367609625645394607749800"
}
],
"has_more": true,
"next_cursor": "1792714280000:0x9bb2ac747df3557a00c1bfe4bc6189daa5289f31eafb7c4f32bae6071a341f2a"
}Variations
Save the outcomes for DuckDB. One line per market, with the Up token and the winner.
# markets.jsonl: condition_id, slug, Up token and winning token of each market
curl -s -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/v1/polymarket/settlements/bulk?slug_prefix=btc-updown-5m-&from_ms=1792713600000&to_ms=1792800000000&limit=5000" \
| jq -c '.settlements[] | select(.outcomes[0] == "Up") | {condition_id, slug, up_token: .token_ids[0], winning_token_id}' > markets.jsonlDownload the day's quotes.
# the top of book of every 5-minute BTC market on 2026-10-23 (186 MB)
curl -s -L -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/v1/polymarket/archive/url?date=2026-10-23&stream=l1_quotes&asset=btc&tenor=5m&redirect=true" \
-o btc-5m-l1_quotes-2026-10-23.parquetThe backtest in DuckDB. An example rule: buy Up at the ask 60 seconds before the close when the ask is between 0.55 and 0.90, and hold to
resolution. The ASOF join takes the last quote received at or before the decision.
WITH m AS (
SELECT *, (CAST(split_part(slug, '-', 4) AS BIGINT) + 300) * 1000 - 60000 AS decide_ms
FROM read_json_auto('markets.jsonl')
),
q AS (
SELECT token_id, local_ts, CAST(ask AS DOUBLE) AS ask, CAST(ask_size AS DOUBLE) AS ask_size
FROM read_parquet('btc-5m-l1_quotes-2026-10-23.parquet') WHERE ask > 0
),
entries AS (
SELECT m.slug, q.ask, q.ask_size, m.decide_ms - q.local_ts AS quote_age_ms,
CASE WHEN m.winning_token_id = m.up_token THEN 1.0 ELSE 0.0 END AS payout
FROM m ASOF JOIN q ON m.up_token = q.token_id AND m.decide_ms >= q.local_ts
)
SELECT count(*) AS trades,
avg(payout) AS win_rate,
avg(payout - ask) AS pnl_per_share_before_fees
FROM entries
WHERE quote_age_ms < 5000 AND ask BETWEEN 0.55 AND 0.90;Full depth instead of the touch. For orders bigger than the best level, replay the book_events file (1.65 GB that day) and walk the ladder.
# where the day's full-book file is, with its size
curl -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/v1/polymarket/archive/url?date=2026-10-23&stream=book_events&asset=btc&tenor=5m"Before you rely on it
Lookahead, fills and fees are yours to model
Decide on local_ts, the time a quote reached the recorder, and use only quotes at or before the decision; the
settlement row is the payoff, never an input. A taker fill at the ask is real only up to ask_size; a resting order's
place in the queue is not in the data. Nothing in the tape or the archive carries fees, so subtract the venue's fee
schedule yourself.
Stale quotes after a gap
Where the recorder lost the feed, book_events has status rows gap_start and live_resume, and the quotes in
between are missing. Require a fresh quote, as the query does with quote_age_ms < 5000, or skip those stretches.
Ask your agent
Connected to the Vike MCP server (https://vike.io/mcp) in Claude, ChatGPT or Cursor, ask it in plain words. The polymarket_archive tool answers with the download links, at 1 credit a call. The backtest runs on your side.
Give me the download links of the Polymarket 5-minute BTC top-of-book and full-book files for 2026-10-23