Two Polymarket strategies that look fine until you add fees and a queue
Filling at the mid and leaving out fees makes it pretty easy to get a profitable-looking backtest for a short-horizon Polymarket strategy. We wanted to see how much two simple ideas change once the backtest pays the taker fee, walks the book for depth, and makes resting orders wait in the queue.
Both tests ran on one day (2026-09-08) of Polymarket's BTC 5-minute Up/Down markets. The data has order book snapshots and deltas for the UP and DOWN tokens, trade prints, and the Chainlink stream the markets settle on. The book is rebuilt from snapshots and from deltas thinned to a capture cadence, so it approximates the depth at each moment rather than replaying the exchange exactly.
An 82% win rate that loses money
In the last 60 seconds of each market, the first strategy buys whichever side is priced higher, but only between 55¢ and 95¢:
from outcometick import Strategy, Order
from outcometick.backtest import run, load_sample
class Favourite(Strategy):
def on_market_open(self, ctx, market):
self.close, self.done = market.close_ts_ms, False
def on_tick(self, ctx, tick):
if self.done or self.close - ctx.now > 60_000:
return None
book = ctx.book()
up, down = book.best("UP"), book.best("DOWN")
if up is None or down is None:
return None
self.done = True
side, price = ("UP", up) if up >= down else ("DOWN", down)
if not 0.55 <= price <= 0.95:
return None
return Order(side=side, notional=50, limit=0.95)
result = run(Favourite, load_sample("polymarket"))
print(result.summary())market-days 1 net pnl -$15.62 trades 108 win rate 82.4% brier 0.1397 edge/contract 0.7¢ max drawdown -$284.51 fees -$52.90 (venue schedule, estimated per book level) lost to slippage -$4.22
It won 82.4% of 108 trades, and the edge before fees was 0.7¢ per contract. Polymarket charges takers C × rate × p × (1 − p) with a rate set per market, and that came to $52.90 for the day, more than the strategy made before fees. When you buy at 80–95¢, a high win rate is mostly what you paid for. Here, the 82.4% win rate still left a net loss of $15.62.
Maker quotes that fill at the wrong moments
For the second test, we tried resting orders to avoid the taker fees. This strategy quotes 25 shares of UP at the best bid and 25 of DOWN at one minus the best ask. It requotes at most once a second and exits inventory with a passive order one tick above entry, with 150 ms order latency and 100 ms cancel latency. It is maker_quotes.py from the backtester's examples.
To be filled, a resting order has to wait until trades have used up the size queued ahead of it at that price. The two books are mirrored: an UP offer at a and a DOWN bid at 1 − a are one queue, published twice, so the DOWN quote waits behind the UP offers at the best ask. Once it fills, a markout measures how far the mid has moved against you 1s, 10s and 60s later, and at settlement.
trades 1105 net pnl -$26,133.42 rested 117061 orders, maker fill rate 0.0377 by size median queue ahead at join 94.57, at fill 0.0 markout 1s: -0.0463 per share over 44594 fills markout 10s: -0.0408 per share over 43760 fills markout 60s: -0.0397 per share over 33437 fills markout settle: -0.0462 per share over 44606 fills
Here a trade is one position from open to close, so the 1,105 trades were built from about 44,600 individual maker fills. The 1s markout averaged −4.6¢ a share and was still about −4.6¢ at settlement. A quote this simple tends to get filled when the other side knows something, for example when BTC has just moved on spot and the 5-minute market hasn't repriced yet. Those fills lost far more than the spread earned.
Only 3.8% of the quoted size filled, and the median fill had about 95 shares ahead of it when the order joined the queue. If the backtest had filled each quote as soon as the price touched it, this would have looked like a strategy that earns the spread on every fill.
A checklist for any Polymarket backtest
When we look at a Polymarket backtest, these are the things we check:
- Fees per market. The taker fee depends on price and on each market's own rate. Assuming a flat fee, or no fee at all, overstates the returns of any strategy that crosses the spread.
- Book depth. Fill an order level by level against the book as it stood at that moment, and report the size the book didn't have instead of filling it at the best price.
- Latency. Match the order against the book as it was when the order would have arrived, not when the signal fired.
- A queue for resting orders. A price touch is not a fill: trades still have to use up the size ahead of your order. Skipping that wait makes maker strategies look far better than they are.
- Markouts. For maker orders, also check how the mid moves after a fill. The 1s markout helps explain the PnL by showing whether the price moved against the order almost immediately.
- Settlement from the official outcome. These markets settle on a Chainlink stream, and the market's own config says which one. Settling on a spot price you picked yourself can mislabel close markets.
- Out-of-sample days. One day is not enough to draw conclusions. Rank parameters on earlier days, then test the winners on later days that weren't used for ranking.
Reproducing this
The code for these tests is in quickstart.py and maker_quotes.py in polymarket-backtest. Install it with pip install outcometick; load_sample() downloads the current free sample day, which was 2026-09-08 for the figures above. The sample day changes from time to time, so a newer one prints different numbers. Each run took about two minutes on an 8-core machine.
The backtester is MIT-licensed, and there's also a JS version. The sample data is free under CC BY 4.0, requires no key or signup, and is also on Kaggle and Hugging Face. For the out-of-sample check, use sweep(..., holdout=0.3); to run the same strategy class over recent weeks without downloading anything, use the hosted backtester.
If you find a maker variant with a non-negative 1s markout on a different day, we'd like to see it on Discord.