/Puzzles
About
Team
Investments
Research
Research Index
Build
IncubationsOpen Source
Writing
Paradigm Puzzles
Hackathon

Terms, Disclosures, Privacy

LinkedIn, Twitter, Contact

← Back to Leaderboard

About the Prediction Market Challenge

The Instrument

You trade a single YES share on a FIFO limit order book. The contract settles to $1 if a latent score process Zₜ finishes above zero at time T, and $0 otherwise. Prices are integer percentage ticks from 1 to 99 - a bid at tick 48 means you're willing to buy YES at 48¢.

Binary contract settlement with probability path50%TimeP(YES)settles $1 if Zₜ > 0

The Challenge

You are a market maker posting passive limit orders on a shared order book. Your goal is to maximize edge - the profit you earn from providing liquidity. You can't send market orders or IOC orders; you can only place resting limit orders and cancel them.

You share the book with a static competitor that maintains a hidden ladder of resting orders around the starting price. The competitor never re-anchors to fair value and never reacts to jumps - it just refills consumed levels at a fixed offset. Your advantage comes from adapting to market conditions it ignores.

Market event loopquotefillsYourStrategyArbsweepRetailflowOrderBook+ edge from retail− edge from arb

How Scoring Works

For each passive fill, edge measures how much better your execution price was than the true probability at the time of the fill:

Buy  q shares at price x:  edge = q × (p_t − x)
Sell q shares at price x:  edge = q × (x − p_t)

where p_t = Pr(Z_T > 0 | Z_t) is the true fair value

Retail fills generate positive edge (you captured the spread). Arb fills generate negative edge (the arbitrageur took stale quotes). Your leaderboard score is the mean edge across 200 simulations with randomized parameters. Higher is better.

Edge accumulation from retail and arb fills$0Fills over timeEdge ($)↑ retail   ↓ arb

The Simulation

Each simulation runs 2,000 steps. Before each simulation, hyperparameters (jump intensity, jump variance, retail arrival rate, competitor spread) are sampled randomly - your strategy doesn't know these values and must adapt from observed fills and order flow.

Latent Score Process

The underlying score follows a Gaussian random walk with compound Poisson jumps:

Z_{t+1} = Z_t + σ·ε_t + Σ J_{t,k}

ε_t ~ N(0, 1)            Gaussian diffusion (fixed σ = 0.02)
N_t ~ Poisson(λ_jump)    jump count per step
J_{t,k} ~ N(μ_jump, σ_jump²)   jump sizes

The diffusion term is fixed across all simulations. The regime-level variation comes entirely from the jump parameters - intensity, mean, and variance - which are randomized per simulation. With ~2 expected jumps per simulation at default intensity, each jump is a significant information shock.

Latent score process with jumpsZ=0JJJTimeZ(t)Zₜ₊₁ = Zₜ + σε + ∑J

True Probability

The informed fair value at step t with H steps remaining is:

p_t = Pr(Z_T > 0 | Z_t)
    = Σ Poi(n; λ·H) × Φ((Z_t + n·μ) / √(H·σ² + n·σ_jump²))

Your strategy does not receive pₜ or Zₜ. You must infer market conditions from fills, your inventory, and the competitor's best bid and ask.

Event Loop

Each step follows this sequence:

  1. Competitor replenishments from previous step
  2. Your on_step callback - you see fills from last step and place new orders
  3. Latent score advances to Zₜ, true probability pₜ computed
  4. Arbitrageur sweeps all stale quotes vs pₜ
  5. Retail market orders arrive and match against the book

You quote before the next price move, so you are always exposed to adverse selection. The arb executes before retail, matching the AMM challenge structure.

Counterparties

Three other actors trade on the book:

  • Arbitrageur: Knows pₜ. Sweeps every resting ask below pₜ and every resting bid above pₜ. Infinite capital, never rests orders.
  • Retail: Uninformed flow arriving via Poisson process. Equal probability buy or sell. Notional is LogNormal-distributed. This is your profit source.
  • Competitor: Static hidden-liquidity ladder. Quotes every tick outside its spread with fixed notional. Refills consumed levels at a fixed offset next step. Never re-centers.

Writing a Strategy

Strategies are written in Python and must extend BaseStrategy. You implement a single method, on_step, called once per timestep with your current state. Return a list of actions: place limit orders, cancel specific orders, or cancel all.

on_step

def on_step(self, state: StepState) -> Sequence[Action]:
    # state contains:
    #   step: int                    current tick number
    #   steps_remaining: int         steps until settlement
    #   yes_inventory: float         your YES holdings
    #   no_inventory: float          your NO holdings
    #   cash: float                  total cash
    #   reserved_cash: float         cash locked in resting orders
    #   free_cash: float             available cash
    #   competitor_best_bid_ticks: int | None
    #   competitor_best_ask_ticks: int | None
    #   fills: tuple[Fill, ...]      fills from last step
    #   own_orders: tuple[OwnOrderView, ...]

    return [CancelAll(), PlaceOrder(side=Side.BUY, price_ticks=p, quantity=q), ...]

You start with $1,000 cash, 0 YES, and 0 NO. Minting one YES and one NO costs $1. Resting bids reserve price × quantity cash; uncovered asks reserve (1 − price) × quantity.

Example: Simple Ladder

from orderbook_pm_challenge.strategy import BaseStrategy
from orderbook_pm_challenge.types import CancelAll, PlaceOrder, Side, StepState

class Strategy(BaseStrategy):
    """Minimal static ladder strategy."""

    quote_size = 5

    def on_step(self, state: StepState):
        competitor_bid = state.competitor_best_bid_ticks or 49
        competitor_ask = state.competitor_best_ask_ticks or 51
        midpoint = (competitor_bid + competitor_ask) // 2

        actions = [CancelAll()]

        if state.yes_inventory < 100:
            actions.append(
                PlaceOrder(
                    side=Side.BUY,
                    price_ticks=max(1, midpoint - 2),
                    quantity=self.quote_size,
                )
            )

        if state.free_cash > 0 and state.no_inventory < 100:
            actions.append(
                PlaceOrder(
                    side=Side.SELL,
                    price_ticks=min(99, midpoint + 2),
                    quantity=self.quote_size,
                )
            )

        return actions

Rules & Constraints

  • Passive only: Limit orders only - no market orders, IOC, or hidden orders
  • Rate limit: 10 submissions per hour
  • Budget: $1,000 starting cash, collateral reserved per resting order
  • Privacy: Strategy code is private - only you can see yours
  • Ranking: Highest mean edge across 200 simulations wins
  • Exceptions: Uncaught exceptions end the simulation in failure
Submit Your StrategyGitHub Repo
LeaderboardSubmitGitHubAbout
Benedict Brady
Dan RobinsonParadigm
LeaderboardSubmitGitHubAbout
Benedict BradyDan Robinson