DEV Community

Cover image for Backtesting live football on Polymarket: 6 traps in a recorded order book
ekocam
ekocam

Posted on Originally published at orcalayer.com

Backtesting live football on Polymarket: 6 traps in a recorded order book

We recorded the order book of every Polymarket football (soccer) market for three weeks and tested two in-play ideas: buying a team right after a late price jump at a level score, and protecting goal bets with a stop-loss. Neither paid. Buying the jump 2 seconds later lost $2.85 per $10 over 837 jumps (t -7.9), and every one of 28 stop settings did worse than holding on the replay. The full results are on our blog and the data is on GitHub.

This post is about the data side: six traps we hit while building the test. Most of them will bite anyone who backtests in-play markets on Polymarket.

1. You have to record the book yourself, then check the recording

Public Polymarket APIs give you trades and the current book, not the book as it was a second ago. For an in-play test you need the best bid and ask around every event, so we recorded the CLOB WebSocket market channel (book, price_change, last_trade_price) with exchange timestamps.

Before using it, check it against on-chain trades. Ours had 99.9% of on-chain trades, and 97.6% of them happened at the best price the recording showed at that moment. If your numbers are much lower, your replay will invent fills.

2. Events arrive out of order

The exchange can send the price_change for a level that a trade just ate before the last_trade_price of that same trade. Read events in arrival order and fills look about 25% worse than they were. Put each trade ahead of the book change it caused before you measure anything.

3. The mid is not a price you can trade

Our first detector watched the mid-price: if a team's mid jumps by 0.25 or more late in a level match, call it a goal. Many of those "goals" were sellers pulling their quotes. The mid doubles, the bid stays where it was, and the spread opens up.

One example from the data: the mid went from 0.24 to 0.505 in seconds, but the ask was 0.78 and the bid about 0.23. A bot buying 2 seconds later paid 0.81. Ten seconds after the jump the ask was back at 0.25, and the match ended in a draw.

Store the ask and the spread, not only the mid. The implied bid is easy to get back if you only kept mid and ask:

def spread_at_jump(mid: float, ask: float) -> float:
    bid = 2 * mid - ask          # implied bid
    return round(ask - bid, 2)   # e.g. mid 0.505, ask 0.78 -> 0.55
Enter fullscreen mode Exit fullscreen mode

In our data 627 of 837 late jumps had a spread of 20 cents or more, and the jumping side won only 36% of those.

4. The "level score" can be fake too

Our rule also needed the draw to be the favorite 30 seconds before the jump. That condition is read from mids as well, so pulled quotes can fake it in a match one team already leads. When all three books of a 1X2 match are two-sided, the three mids add up to about 1.00. Check that before you trust the state:

def book_looks_clean(home_mid: float, draw_mid: float, away_mid: float) -> bool:
    return 0.95 <= home_mid + draw_mid + away_mid <= 1.05
Enter fullscreen mode Exit fullscreen mode

In 51 of the 85 jumps that had a tight spread, the three mids before the jump added up to more than 1.10. One of them was a team that had led for 20 minutes: its bid disappeared for a moment, the score looked level, and the bid coming back looked like a late goal.

5. Daily batch files overlap

We ran the scan one day at a time, and each day file also covered the first hours after midnight UTC. Jumps after midnight landed in two files: 48 duplicates out of 885 raw rows. They did not change the conclusion, but they would have inflated every count and t-statistic. If you split a time series by day, check the joins.

6. The fastest entry is not reachable

Football markets have secondsDelay: 1 in the Gamma API: a taker order waits a second before it can match, and you cannot cancel it during that time. So a result "at +0 s" is not a strategy, it is a plot point. In our test the earliest entries lost the most (-$4.21 per $10 at +0 s, -$2.85 at +2 s), because the first seconds after a jump are when the price is most wrong in the direction you are buying.

The same delay hurts stops. Polymarket has no stop orders, so a stop is a bot watching trades and then market-selling after the delay, paying the taker fee a second time. In live football prices swing several cents within minutes, and a 3-cent stop fired on 86 to 96% of our replayed bets.

Reproduce it

The repo has data/late_goal_jumps.csv (one row per jump, with the asks 0 to 60 seconds after it and the result) and a standard-library script that recomputes every late-goal number:

python3 scripts/summarize_late_goals.py data/late_goal_jumps.csv
Enter fullscreen mode Exit fullscreen mode

METHOD.md describes the recorder, the detection rule and each trap above in more detail.

What this does not show

About three weeks of football, mostly club leagues. Pre-match markets, other sports and stops on strategies whose entries already make money were not tested. Bets in the same match are not independent, so read t values near 2 with care.


Originally published on orcalayer.com. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.

Top comments (0)