Files
2026-07-06 15:28:07 -04:00

5.9 KiB
Raw Permalink Blame History

poly-maker

A maker-only market-making bot for Polymarket CLOB V2, focused on political markets. Single async process, local-file config, typed and tested.

Warning

Market making on Polymarket is competitive and can lose money. This is a reference implementation and a research harness, not a guaranteed-profitable product. Test in --paper mode first; go live with small size.

What it does

  • Discovers political markets via the Gamma API (seconds) and ranks them by reward + rebate income vs. volatility/spread risk.
  • Maintains a live order book per token from the market WebSocket.
  • Quotes maker-only — every order is post-only. Fair-value + inventory-skew strategy that posts BUY-YES and BUY-NO as a two-sided quote, with live volatility/toxicity estimation and a regime machine that pulls quotes during news events (see Strategy).
  • Reconciles a target quote set against live orders with churn tolerances; runs the exchange heartbeat dead-man switch; enforces risk caps and a daily-loss kill switch.
  • Config, market selection, and state are local files + SQLite. An operator with the repo, a .env, and a funded wallet is a complete deployment.

Install

Uses uv and Python 3.12+.

uv sync --extra dev          # install deps + dev tools
uv run polymaker --help

Configure

cp .env.example .env         # then edit two values:
  • PK — the private key of your signer wallet
  • BROWSER_ADDRESS — your Polymarket address (shown on the profile / developer page)

Everything else is TOML under config/:

  • config.toml — wallet/engine/risk/execution settings
  • strategy.toml — named parameter profiles (political-longdated, political-hot)
  • markets.toml — the trade list (populated via the CLI below)

Use

# 1. discover + rank political markets (writes to state.db)
uv run polymaker scan
uv run polymaker markets

# 2. add markets to the trade list
uv run polymaker markets-add <slug> --profile political-longdated

# 3. dry run: full pipeline against the live feed, no orders posted
uv run polymaker run --paper

# 4. preflight the wallet before going live
uv run polymaker doctor

# 5. self-tests: a deep post-only order (free), then a real fill round-trip (~cents)
uv run polymaker livetest      # place a deep post-only order + cancel (no fill)
uv run polymaker moneydoctor   # limit rest + market buy + market sell, auto-flattens

# 6. go live
uv run polymaker run

# ops
uv run polymaker status        # positions / open orders
uv run polymaker cancel-all    # panic button

Architecture

market WS ─▶ OrderBook ─▶ (wake) ─▶ Quoter ─▶ strategy (pure) ─▶ reconcile ─▶ ExecutionGateway
user WS   ─▶ StateStore                                         RiskManager ┘   (post-only, heartbeat)
Gamma     ─▶ Catalog/scanner ─▶ SQLite            periodic REST reconcile ┘

One async event loop. The strategy layer is a pure function (book, inventory, params, clock) → TargetQuotes — deterministic and unit-tested. The engine owns all I/O and state around it; the ExecutionGateway wraps py-clob-client-v2 (which handles the V2 EIP-712 signing) and offloads its blocking calls to a thread pool so the hot path never stalls. State (positions, orders, PnL, catalog) lives in one SQLite file; raw WS/order events are journaled to journal/ for replay.

Strategy

Maker-only, quoting both sides of each market as USDC-collateralized bids:

  • Fair value — depth-weighted microprice off the live book, nudged by an EWMA of signed trade flow.
  • Quote construction — reservation price r = FV skew(inventory); half-spread δ = base + c_vol·σ + c_tox·toxicity. Post BUY-YES at r δ and BUY-NO at (1 r) δ. Because both legs are bids that sum below 1, a filled pair merges back to USDC at locked edge 1 p q — a maker-only exit that never crosses the spread.
  • Inventory skew — net position leans both quotes: long YES → bid YES lower, bid NO higher (acquire the offsetting leg). Size tapers as inventory approaches a soft cap, then the adding side is pulled entirely.
  • Volatility / toxicity — realized-vol and per-fill markout (adverse selection) EWMAs widen the spread and shrink size in markets that pick us off.
  • Regime machine — per market: QUIET (farm rewards in-band), TRENDING (lean + widen + half size), EVENT (sweep/jump detected → pull quotes, cool off), REDUCE_ONLY (inventory cap / near end date → exits only), HALTED (stale data / resolved / kill switch → cancel all).
  • Rewards + rebates — quotes stay inside the liquidity-rewards band in QUIET; the market selector also scores the new maker-rebate program (a share of taker fees rebated to makers).
  • Risk — per-market notional cap, neg-risk event-group worst-case cap, total exposure cap, daily-loss kill switch, WS-staleness halt.

Tune it all via profiles in config/strategy.toml.

Develop

uv run pytest                 # unit suite (offline)
POLYMAKER_LIVE=1 uv run pytest tests/test_live_marketdata.py   # live WS integration
uv run ruff check src tests   # lint
uv run mypy src               # types (strict)

Status

Implemented and live-verified end to end (auth → book → strategy → sign → post → cancel): config, catalog/scanner, order book + analytics, strategy (FV, vol/toxicity, regime, quoting), state store + lifecycle, execution gateway + reconciler + heartbeat, market/user websockets, risk manager, merger (EOA path), engine, CLI, paper mode, journal capture. 83 tests; ruff + mypy strict clean.

Not yet built: a replay backtester over the captured journals, and external data feeds (polls / news / cross-venue). Merging through a Safe/proxy wallet routes a tx via the relayer and isn't wired yet — until then inventory exits via limit sells rather than merging.

License

MIT