Skip to main content

Trade Execution Internals

PolySimulator aims to replicate Polymarket’s execution semantics as closely as possible while providing instant fills for paper trading. This document describes exactly how fill prices are determined.

Execution Priority Cascade

When you submit a trade, the execution engine evaluates price sources in this order: Each layer has sanity guards that reject fills diverging too far from the cached display price. The threshold is relative and market-kind-aware: 15% for ordinary markets, but wider for fast binary Up/Down markets that legitimately swing near resolution (25% for hourly/daily Up/Down, 30% for 15-minute, 50% for 5-minute). If all layers fail, the trade is rejected with Price unavailable for market.

Layer 1: Order Book Walk (VWAP)

For size-aware execution, the engine walks the order book to compute a Volume-Weighted Average Price (VWAP).

How It Works

In every case the walk consumes the best price first, then steps to the next-best level until your quantity is filled: The engine accumulates fills level-by-level until the requested quantity is satisfied:

Complement-Aware Execution

PolySimulator replicates Polymarket’s dual-book matching. In binary markets, you can fill a BUY by:
  1. Buying the primary token from its ask side, OR
  2. Selling the complementary token from its bid side
The effective price conversion:
  • effective_ask = 1 - complementary_bid
  • effective_bid = 1 - complementary_ask
Both books are merged and sorted before walking. This prevents thin primary books from producing absurd fills when the complement has better liquidity.

Sanity Guards

The book walk is rejected if the VWAP falls outside a symmetric relative band around the cached display price, cached × [1 − t, 1 + t], where t is the market-kind-aware threshold (default 0.15; wider for Up/Down — see above). Both sides use the same band: (An earlier revision mixed a multiplicative upper bound with an absolute cached − 0.15 lower bound; that asymmetric form was replaced because the absolute term effectively no-op’d the guard for low-priced outcomes. The guard is now purely relative so it scales correctly across [0.01, 0.99].)

Layer 2: Best Bid/Ask

If the order book walk is unavailable, the engine uses the top-of-book prices:

Complement Merging

The best bid/ask is computed from both the primary and complementary order books:

Sanity Guards

If the selected fill price diverges beyond the same relative threshold from the cached outcome price (15% default; wider for Up/Down):
  1. First, try the primary-only best bid/ask (ignoring complement)
  2. If that also diverges, reject the fill and fall through to the next layer
This prevents the complement-book logic from accidentally inverting fills on high-probability outcomes.

Layer 3: CLOB Midpoint Cache

The engine caches the latest CLOB midpoint for each token. The midpoint is (best_bid + best_ask) / 2 from Polymarket’s live order book, refreshed roughly every 30 seconds. Latency: Sub-millisecond.

Layer 4: Cached Outcome Price

The final fallback uses the cached display price — the same price visible in the UI.

Label Matching

The engine matches the requested outcome label ("Yes", "Up", "Down", etc.) against the market’s outcome array:
This ensures label → price mapping is correct even when the upstream buy/sell fields don’t correspond positionally to outcomes[0]/outcomes[1].

Fallback Chain

If direct label matching fails:
  1. Try conventional aliases (yes/no → first/second outcome)
  2. Compute midpoint from best_bid + best_ask fields
  3. Use last_trade price if spread is wide (>10%)
  4. Average yes_price + no_price

Limit Order Enforcement

For limit orders, the fill price is validated against your specified limit:
FOK Semantics: If the market price has moved past your limit, the order is rejected outright with FOK_ORDER_NOT_FILLED_ERROR. Unlike some brokers, PolySimulator does NOT cap your fill at the limit price. This matches Polymarket’s IOC/FOK behavior.

Price Source Tracking

Every fill records which price source was used: This is returned in the order response and recorded server-side for auditing.

Spread & Impact Metrics

Each fill computes: These are available in the order response and help you understand execution quality.

Market Validation

Before execution, the engine validates:
  1. Not closed: closed=false
  2. Active: active=true
  3. Not resolved: resolved_outcome is null
  4. Not expired: end_date is in the future (or SELL for emergency exit)
  5. Price sanity: Outcome prices don’t sum > 1.5 (post-expiry detection)
Emergency Exits: When selling a position on an expired market with no live price, the engine allows a break-even exit at your entry price rather than blocking the trade.

Idempotency

Every trade requires a client_order_id (or Idempotency-Key header). Duplicate submissions return the original order without re-executing. This is critical for:
  • Retry-safe bot execution
  • Preventing accidental double-fills on network timeouts
  • Audit trail integrity

Example Execution Flow