The difference between a trader who captures edge and one who leaks it to the market isn't usually the model — it's how they execute. Two people with identical prediction models can have wildly different P&L, and most of that gap comes from how they read the order book at the moment of entry.
This post explains what an order book actually tells you on Polymarket, which signals indicate fills are safe versus risky, and how professional traders use depth-of-market data to decide when to fire, when to post passively, and when to pass entirely.
What an Order Book Shows You
Every Polymarket contract has a central limit order book (CLOB) with:
- Bids: buy orders, sorted from highest price to lowest
- Asks: sell orders, sorted from lowest price to highest
- Size at each level: how many contracts are offered at that price
- The spread: best ask minus best bid
- The mid: (best bid + best ask) / 2
A simplified snapshot for a single YES contract:
Best ask is 0.65, best bid is 0.63, spread is 0.02 (2 cents), mid is 0.64.
Signal 1: The Spread
The spread is the cheapest reading of liquidity. Tight spreads (1-2c) on sports markets mean a market maker is actively quoting both sides and is confident enough to offer close prices. Wide spreads (5c+) mean either low liquidity, uncertainty, or both.
| Spread | Read | Action |
|---|---|---|
| 1-2c | Tight, market maker present | Safe to take. Low slippage risk. |
| 3-4c | Normal for sports in-play | Trade at small-to-medium size. |
| 5-6c | Wide. Either pre-game or volatile state. | Small size only, or post passive bid. |
| 7c+ | Very wide. Market maker offline or market confused. | Skip or post a passive bid 1c inside. |
A trading rule that saves more money than it costs: cap your entry at spread ≤ 6c. Wider spreads mean you're paying the spread as a fixed cost on top of whatever edge you have.
Signal 2: Top-of-Book Size
Size at the best ask tells you how much you can take before moving to the next price level. If the best ask is 12 shares and you want to buy 50, you'll "walk the book" and pay progressively higher prices for 38 of your 50 shares.
For the book above:
- Buying 10 shares: all fill at 0.65. Average price 0.65.
- Buying 50 shares: 12 at 0.65, 38 at 0.66. Average 0.658.
- Buying 100 shares: 12 at 0.65, 45 at 0.66, 43 at 0.67. Average 0.664.
That 0.014 difference between 10-share and 100-share fills is pure slippage. If you thought you had a 5c edge at the best ask, buying 100 shares cuts your effective edge to 3.6c. On highly illiquid markets, a 100-share order might consume half your edge.
Signal 3: Book Imbalance
Compare total size on the bid side to total size on the ask side (across top 3-5 levels). Heavy imbalance signals market-maker inventory skew.
Heavy bid-side size (buys lined up, few sellers)
The market maker is short the contract and defending higher prices by refusing to quote low asks. Your next market-buy order will push price up. Bad time to take at the ask.
Heavy ask-side size (sells lined up, few buyers)
Market maker is long and trying to offload inventory. Your ask-taking fills cleanly. Good execution environment.
Balanced book
Normal. Size both sides, tight spread. Any fill should work at stated prices.
Compute it as a ratio:
def book_imbalance(book: dict, levels: int = 3) -> float:
"""Returns ratio of bid size to ask size over top N levels.
> 1 = bid-heavy (harder to buy cheap). < 1 = ask-heavy (easy to buy).
"""
bid_sz = sum(b["size"] for b in book["bids"][:levels])
ask_sz = sum(a["size"] for a in book["asks"][:levels])
if ask_sz == 0:
return float("inf")
return bid_sz / ask_sz
# Example
if book_imbalance(book) > 2.0:
# Bids >> asks. Expect upward price pressure. Skip taker buys.
skip("book-imbalance")
Signal 4: Level Stickiness
A price level that's been sitting on the book for minutes with real size is a hard level. A level that flickers in and out is a soft level. You want to take liquidity at hard levels — they're less likely to move away when your order hits.
In practice, track the persistence of top-of-book levels across snapshots:
class LevelTracker:
def __init__(self):
self.first_seen: dict = {} # (price, side) -> first_seen_ts
def observe(self, best_ask_price: float, ts: float):
key = ("ask", best_ask_price)
if key not in self.first_seen:
# New level, possibly soft
self.first_seen[key] = ts
# Return how long this level has been visible
return ts - self.first_seen[key]
# If the level has been > 3 seconds old, it's relatively hard. Safer to take.
Signal 5: Quote Age / Staleness
Between the moment the market maker's quote was posted and the moment you hit it, new information may have arrived. On sports markets, a score change after the quote but before your fill creates two scenarios:
- Score change favors your side → market will reprice higher, your entry fills at a bargain
- Score change against your side → market will reprice lower, your entry fills at what's now a bad price
You can't know which direction ahead of time, but you can skip trades where the quote is stale. If the best ask was posted 15+ seconds ago and hasn't updated, and a score just changed, the market maker is about to move that quote. Don't race them.
def is_quote_fresh(book: dict, max_age_s: float = 5.0) -> bool:
"""Is the top-of-book fresh enough to trust?"""
age = time.time() - book.get("last_update_ts", 0)
return age < max_age_s
When to Post Instead of Take
If the book is wide but your edge is real, you can post a bid 1-2 cents inside the current best ask and wait for a seller to hit you. This captures the spread instead of paying it — a real edge multiplier for a high-frequency bot.
The tradeoff: you may not fill at all. The decision:
| Spread | Your edge | Take or post? |
|---|---|---|
| ≤ 3c | ≥ 8c | Take. The spread cost is tiny. |
| 4-6c | ≥ 8c | Consider posting at best_ask − 1c. |
| 4-6c | 5-7c | Post passive. Taking eats most of your edge. |
| 7c+ | Any | Post or skip. Taking is expensive at any edge. |
Putting the Signals Together
A practical entry filter combining all five:
def entry_safe(book: dict, order_size: int) -> tuple[bool, str]:
"""Returns (can_enter, reason). Trade only if can_enter."""
spread = book["asks"][0]["price"] - book["bids"][0]["price"]
best_ask_size = book["asks"][0]["size"]
imb = book_imbalance(book)
age = time.time() - book["last_update_ts"]
if spread > 0.06:
return False, f"spread_too_wide({spread:.3f})"
if order_size > best_ask_size * 0.5:
return False, f"size_too_big({order_size} vs {best_ask_size})"
if imb > 2.5:
return False, f"bid_heavy_book({imb:.1f})"
if age > 5.0:
return False, f"stale_quote({age:.1f}s)"
return True, "ok"
This kind of pre-trade check is what separates a bot that leaks 2-3c to bad fills on every trade from one that captures the edge it calculates.
Get calibrated signals + execution guidance. ZenHodl's API includes pre-trade book quality checks alongside live win probabilities for 11 sports.
See ZenHodlFurther reading: Kelly Criterion for Position Sizing · Risk Management for Bots · Finding Edge in Prediction Markets