BANGER
2026-06-01 · 6 min read

How to Backtest a Prediction-Market Strategy

Backtesting a prediction-market strategy looks like backtesting equities until you hit the first contract resolution. Then the differences stop being cosmetic. A YES contract does not drift forever. It terminates at exactly $1.00 or $0.00 on a known date, and that single fact reshapes how you build, validate, and trust a backtest. This post covers what changes, the data you actually need, the pitfalls that quietly inflate your results, and why a backtest alone is never enough.

Why binary resolution changes the problem

An equity has no terminal value. You mark it to the last trade and the backtest is a sequence of price changes. A prediction-market contract is a binary claim with a hard expiry. On Kalshi, a winning contract settles at $1 and a losing one at $0, resolved against named source agencies filed with the CFTC as part of each contract's terms. On Polymarket, resolution runs through UMA's optimistic oracle: a proposer posts the outcome with a bond (roughly $750 pUSD), a challenge window opens (two hours by default, longer for some higher-value markets), and the market settles if uncontested. A dispute escalates to a UMA token-holder vote and can take days.

Three consequences follow directly:

The data you actually need

Free price history gets you started and then quietly lies to you. Four inputs matter, in rough order of how often they are missed:

The pitfalls that inflate results

The classic biases all have prediction-market-specific shapes. Watch for these:

A minimal backtest loop

Stripped down, an honest backtest does roughly this per market:

for market in universe:                # full universe, including voided/disputed
    for t, book in market.snapshots():  # tick or interval book snapshots
        signal = strategy(book, t)      # only data available at time t
        if signal:
            fill = simulate_fill(book, signal.size)  # walk the book, not the mid
            fill.cost += venue_fees(fill)
            positions.append(fill)
    settle(positions, market.resolution)  # mark every position to $1 or $0

report(hit_rate, avg_payoff, outcome_distribution)  # not Sharpe

The two lines that separate a real backtest from a fantasy are simulate_fill walking actual depth and settle using the true resolution. Everything else is bookkeeping.

Why paper trading is the required complement

A backtest answers one question: would this rule have worked on past data, under my modeling assumptions. It cannot tell you whether your fill model matches the live book today, whether your latency lets you actually hit the prices you saw, or whether liquidity has dried up since your sample. Prediction-market books are often thin, and the gap between assumed slippage and real slippage is exactly where a backtested edge dies.

Paper trading closes that gap. Running the strategy forward against the current live order book, with no capital at risk, surfaces the execution reality a historical backtest cannot. The honest sequence is: backtest to reject obviously broken ideas cheaply, then paper trade the survivors against live markets to see whether the edge holds when the book is real and moving, then go live small. That is the loop Banger runs (write a Python strategy, paper it against the live Polymarket and Kalshi books, promote it to live under a declarative risk envelope once it has earned it, with your own venue keys), but the sequence matters more than the tooling.

Backtesting tells you the idea is not stupid. Paper trading tells you it might actually work. You need both before you risk a dollar.

Run your first strategy free

Paper-trade on Polymarket and Kalshi in minutes, with your own keys.

Start free