Skip to content

Repository files navigation

polymaker

A maker-only market-making bot for Polymarket CLOB V2. It picks its own markets from the Gamma API, quotes both sides post-only around a microprice fair value, and earns liquidity rewards while managing inventory. Single async process, Postgres state.

Warning

This is a research project, not a proven money-maker. Live testing on a small wallet showed the strategy can lose more to adverse selection (informed traders picking off resting quotes) than it earns back in liquidity rewards — the rewards alone did not cover the losses. Nothing here is investment advice, and past behavior on Polymarket is no guarantee of future behavior on any market. Read the code, don't just run it. If you do run it, use money you can afford to lose in full, start at the smallest size the venue allows, and treat the paper/live gates below as a safety net, not a substitute for understanding what the bot does.

The engine runs in paper mode (simulated fills, no real orders) until two independent gates are cleared (TRADING_HALTED=0 and MAKER_LIVE=1).

What it does

Polymarket pays liquidity rewards to makers who rest orders close to the midpoint of a market's order book — the tighter and larger the resting order, the bigger its share of that market's daily reward pool. This bot tries to collect that income on autopilot: it scans the market catalog, picks a handful of markets worth quoting, and continuously posts two-sided limit orders (a buy on YES and a buy on NO) sized and priced to score rewards, while trying to keep its own directional risk small.

The catch, and the reason for the warning above: resting orders can also be picked off. A trader with better or faster information can trade against a stale quote before the bot reprices, and that adverse selection is a real cost that has to be measured against the reward income, not assumed away. Whether the balance is favorable depends on which markets are quoted and how aggressively — which is most of what the code below is trying to get right.

Market selection

Not every market is worth quoting, and picking a bad one is not a rounding error — at low market count the top of the ranking basically is the strategy. Selection happens in two stages:

  1. Scanning (maker/catalog/scoring.py) — a cheap, no-book-fetch score over the whole Gamma catalog, combining reward density with a rebate estimate and a spread penalty. This is only a proxy: it decides which markets are worth the cost of a real book fetch, not which one ultimately gets picked.
  2. Feasibility (maker/catalog/feasibility.py) — for the shortlisted markets, fetch the live order book and compute the actual expected reward given competing maker liquidity already resting in the reward band. A market can look great on the cheap score and pay nothing once the real competition is accounted for.

On top of both, maker/catalog/selection.py applies hard eligibility floors — minimum liquidity, minimum time to resolution, maximum recent price volatility relative to the reward band, and a few category exclusions for markets prone to sudden, hard-to-price jumps (e.g. live-event and breaking-news-driven markets). A market below any floor is unselectable no matter how attractive its score is. What clears the floors is then ranked by the measured dollar estimate, not the cheap proxy, and rotated in and out slowly on purpose — churn (cancel/replace, requoting) has its own cost in lost queue position and reward accrual.

The quoting algorithm

The core pricing logic lives in maker/strategy/quoting.py and is a pure function: (market state, inventory, params) -> target quotes, with no I/O, so it's fully unit-testable.

reservation_price = fair_value - inventory_skew
half_spread       = base + c_vol * short_term_volatility + c_tox * toxicity_estimate
YES bid           = reservation_price - half_spread
NO  bid           = (1 - reservation_price) - half_spread

Both legs are bids — the bot never sells YES or NO directly to enter a position, it always buys both sides. A filled matching pair (some YES + some NO) can be merged back into risk-free USDC collateral, which is what makes two-sided quoting close to self-funding when it works. fair_value is a microprice estimate nudged by recent order-flow direction; the half-spread widens with measured short-term volatility and a running "toxicity" estimate (how much the market has tended to move against the bot's own recent fills), so the bot backs off automatically in choppier or more adverse conditions. In calm regimes, quotes are pulled in toward the reward program's own scoring band so they actually earn rewards rather than sitting uselessly wide.

Above the pure pricing function, the engine (maker/engine/) adds regime classification (quiet / trending / event-driven), inventory and exposure limits, stale-mark and kill-switch checks, and order lifecycle management (place, cancel, reconcile against the exchange).

Risk controls

All risk-appetite numbers — position size, daily loss limit, cumulative drawdown limit, per-market caps, stale-mark timeouts — live in one file, maker/presets.py, deliberately separate from the code that loads them. There is no partial config: every knob has to be set explicitly or the process refuses to start.

Main quote cycle

flowchart TD
    MD[Market Data In<br/>book WS + trade prints] --> WAKE[Wake Quoter<br/>debounce / slow tick]
    WAKE --> FV{Two-sided<br/>book?}

    FV -- No --> EMPTY[Empty Quote Set]
    FV -- Yes --> PRICE[Fair Value<br/>microprice + flow nudge]
    PRICE --> MARK[Update Marks<br/>+ markouts]
    MARK --> BLIND{Blind or<br/>kill switch?}

    BLIND -- Yes --> PULL[Cancel All Quotes]
    BLIND -- No --> RISK[Risk Assessment<br/>exposure, daily loss, stale marks]
    RISK --> LIM{Within<br/>limits?}

    LIM -- Outside Limits --> RO[Reduce-Only / Halt<br/>exits only]
    LIM -- Within Limits --> REG[Classify Regime<br/>QUIET / TRENDING / EVENT]

    RO --> BUILD
    EMPTY --> RECON
    PULL --> RECON
    REG --> BUILD[Build Quotes<br/>half-spread + inventory skew + ladder]
    BUILD --> RECON{Differs from<br/>resting orders?}

    RECON -- No --> MERGE[Merge YES+NO Pairs<br/>back to collateral]
    RECON -- Yes --> CANCEL[Cancel Stale Orders]
    CANCEL --> PLACE[Place Orders<br/>post-only]
    PLACE --> OM[Order Management<br/>track ids, reconcile vs REST]
    OM --> FILL{Fill<br/>received?}

    FILL -- No --> WAKE
    FILL -- Yes --> POS[Update Position<br/>cash, toxicity, markout]
    POS --> MERGE
    MERGE --> WAKE
Loading

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages