Polybitrage is a high-frequency trading engine for prediction markets. It continuously scans logically related binary markets on Polymarket (and Kalshi), detects risk-defined arbitrage, sizes each opportunity against live order-book depth, and either records it in a paper simulation or executes it through a guarded live path.
The core idea is simple: if one market's outcome is logically contained in
another's, their prices must obey a relationship. When they briefly do not, a
combination of legs (Buy YES(parent) + Buy NO(child), and its N-leg
generalizations) has a guaranteed payout for less than that payout's cost. The
hard part is not the math — it is seeing the mispricing, pricing it against real
depth, and acting before it closes. Polybitrage is built end to end around that
latency budget.
- Paper mode by default. No credentials, no orders — it only reads public market data and simulates fills against the live book.
- Guarded live mode. Real orders are gated behind explicit flags, environment credentials, per-session spend caps, and REST rechecks.
- Instrumented for high-frequency work. Every stage of the tick-to-trade path is measured and exported to Prometheus/Grafana.
Polybitrage is a personal research and engineering project. It is not investment advice and is not a solicitation to trade. See Disclaimer.
| Layer | Role |
|---|---|
| Python 3.11+ | Async scan loop, CLI, market discovery, risk sizing, simulation, reporting |
| Rust | Zero-copy Polymarket CLOB WebSocket client (pyo3 extension), Tokio runtime, lock-free DashMap order-book state |
| C (via Cython) | Compiled hot-path scan classifier (-O3 -march=native), with a pure-numpy fallback |
A single-threaded asyncio loop does the scanning and decision-making, fed by the
multi-threaded Rust client over a GIL-released hand-off so the event loop never
blocks on I/O. Full data-flow and a runtime diagram are in
docs/ARCHITECTURE.md.
The design goal is to take the bot's own latency out of the tick-to-trade path so that capture rate is bounded by the network, not by the code. It gets there: with the WebSocket cache warm, the entire hot path — diffing the changed tokens, classifying the full pair universe in the compiled kernel, sizing, and deciding — runs in microseconds. The only millisecond-scale costs left are round trips to the exchange, which are a physical floor that no software can beat.
All timings come from nanosecond-resolution perf_counter_ns instrumentation;
the latency histograms bucket from 50 µs upward, and a scan is flagged "slow"
only above 5 ms. Figures are indicative, from a development machine against the
live Polymarket CLOB — not a benchmark guarantee.
With the WebSocket cache warm (75 pairs, 32 unique tokens):
| Hot-path stage | Typical |
|---|---|
| Dirty-set diff (changed tokens only) | 3 µs |
| Order-book cache read (Rust DashMap) | 45 µs |
| Classify full pair universe (C/Cython kernel) | 1 µs |
| N-leg scan + filter + depth-aware sizing | 10 µs |
| Full steady-state scan | ~59 µs |
These are round trips to the exchange. They dominate only when a fresh REST read is required, and they cannot be optimized in software — only by co-locating closer to the venue.
| Network-bound stage | Typical |
|---|---|
| Order-book fetch, REST batch | ~220–295 ms |
| REST recheck immediately before entry | one round trip |
| Order round-trip | network-bound |
| Book source | First / fresh scan | Steady-state scan |
|---|---|---|
| WebSocket (Rust cache) | seeds from REST once | sub-100 µs while connected |
| Polling + short cache | ~60–90 ms | ~0 ms on cache hit |
| Polling (REST, no cache) | ~220–295 ms | — |
Warming the WebSocket cache turns a ~240 ms REST round trip into a microsecond-scale in-memory read — roughly three to four orders of magnitude off the dominant cost of every scan.
Everything lives in this one folder — clone it and you are looking at the whole project.
Polybitrage/
├── src/ Application code (Python)
│ ├── main.py CLI entry point (poly-arb / polybitrage)
│ ├── simulator.py Paper/live scan loop, sizing, risk controls
│ ├── discovery.py Polymarket candidate generation (Gamma API)
│ ├── polymarket/ CLOB + Gamma clients, WS clients, live trader, models
│ ├── kalshi/ Kalshi REST + FIX clients, discovery, live trader
│ ├── worldcup/ World Cup market relation scanner
│ ├── fast/ Cython/C hot-path classifier + numpy fallback
│ ├── network/ Signal bridge
│ ├── audit*.py Forensic audit capture + profit-leakage reports
│ └── *_profiler.py Latency, market, and system profilers
├── rust_ws_client/ Rust pyo3 WebSocket market-data extension (polymarket_rs)
├── config/ Example pair / watchlist configs
├── docs/ Architecture, CLI, N-leg/live, audit, Kalshi, World Cup
├── monitoring/ Prometheus + Grafana stack (docker-compose)
├── scripts/ Network latency probe
├── tests/ pytest suite
├── reports/ Generated CSV / Markdown / audit output (gitignored)
├── assets/ Logo and images
├── .github/ Issue / PR templates, contributing, code of conduct, security
├── pyproject.toml Package + dependency definitions
├── setup_cython.py Builds the C hot-path extension
├── supervisord.conf Process management for headless deployment
├── LICENSE MIT license
└── .env.example Environment variable template
cd Polybitrage
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev]'poly-arb paper-arb-sim --pairs config/watchlist_pairs.yaml --oncepoly-arb paper-arb-sim --pairs config/watchlist_pairs.yaml --budget 100 --book-source websocket --sizing-mode max_profit --entry-threshold 1.0 --min-edge-threshold 0.0025 --max-trade-size 20 --out reports/paper_sim.csv --trades-out reports/paper_trades.csv --save-markdown reports/paper_sim.mdThe console prints only the live essentials each poll (cash, locked capital,
open positions, best pair, distance to entry, action taken). Detailed rows go to
reports/. If --duration-minutes is omitted, it runs until Ctrl-C.
Everything below is configuration — CLI flags plus the YAML files in config/.
No source code needs editing.
config/watchlist_pairs.yaml is the list of
market pairs the bot watches. Each entry carries the two markets' token IDs and
an enabled: flag — set enabled: true on the pairs you want live and false
to park them. Paper mode needs nothing else.
Two commands, run in sequence. Discovery scans Polymarket for candidate pairs;
build-watchlist then ranks them by liquidity and edge into a smaller,
executable list you point the simulator at.
poly-arb discover-tomorrow-crypto-pairs --assets BTC,ETH,SOL,XRP --days-ahead 1 --out config/generated_crypto_tomorrow.yaml
poly-arb build-watchlist --pairs config/generated_crypto_tomorrow.yaml --top-n 10 --out config/clean_watchlist.yamlThese flags bound how much the bot can commit and how good an edge must be before it enters:
| Flag | Controls |
|---|---|
--min-edge-threshold |
Minimum net edge (after fees) required to enter |
--max-trade-size |
Cap on capital spent per single entry |
--max-total-locked-capital |
Cap on total capital locked across all open positions |
--max-open-positions |
Cap on concurrent open positions |
| Task | How |
|---|---|
| Change the book source | --book-source websocket (fast, default) or --book-source polling |
| Run continuously across daily market rollover | Add --continuous-rollover --rollover-time-et 12:00 |
| Check scan speed before going live | poly-arb benchmark-scan --iterations 5 |
| See the full command reference | poly-arb --help and docs/CLI_AND_LIBRARY.md |
| Variable | Purpose |
|---|---|
POLYMARKET_GAMMA_BASE_URL |
Gamma API base (market discovery) |
POLYMARKET_CLOB_BASE_URL |
CLOB REST base (order books) |
POLYMARKET_WS_MARKET_URL |
Public market WebSocket URL |
POLYMARKET_LIVE_TRADING_ENABLED |
Master switch for live mode (false by default) |
POLYMARKET_PRIVATE_KEY |
Wallet key — live only, never commit |
POLYMARKET_API_KEY / _SECRET / _PASSPHRASE |
CLOB API credentials (SDK can derive from the private key) |
POLYMARKET_FUNDER_ADDRESS |
Funding wallet address (live only) |
POLYMARKET_SIGNATURE_TYPE / _CHAIN_ID |
Signing configuration |
Public market data needs no credentials, so paper mode requires nothing here.
.env is gitignored, and no credentials are stored in this repository.
The bot exposes Prometheus metrics at /metrics on its health port. A ready-to
-run Prometheus + Grafana stack auto-provisions a 60-panel dashboard covering
tick-to-trade latency, scanner stage breakdowns, WebSocket health, order-book
microstructure, OS/scheduler jitter, network RTT, and portfolio PnL.
poly-arb paper-arb-sim --headless --profile --health-host 0.0.0.0 --health-port 8765 --pairs config/watchlist_pairs.yamlcd monitoring && docker compose up -dGrafana is then at http://localhost:3000 and Prometheus at
http://localhost:9090. Full metric reference and dashboard walkthrough:
monitoring/README.md.
| Component | Specification |
|---|---|
| Host | Linux, compute-optimized cloud instance (for example AWS EC2), region chosen for lowest RTT to the venue |
| Region choice | Measure candidates with scripts/latency_probe.py and pick the lowest TCP/HTTPS RTT before committing |
| Runtime | Python 3.11+ (Rust toolchain and C/Cython optional, for the native fast paths) |
| Process supervision | supervisord.conf — auto-restart with backoff, log rotation |
| Observability | Prometheus + Grafana via monitoring/docker-compose.yml |
| Health / metrics | GET /health and GET /metrics on the configured health port |
Edit the directory and command lines in supervisord.conf to match your
deployment path, then:
pip install supervisor
supervisord -c supervisord.conf
supervisorctl -c supervisord.conf statusLive mode places real orders with real money and is disabled by default. Do not enable it unless you are legally permitted to trade on the venue and accept full responsibility for the outcome.
Live mode adds several independent guards: the POLYMARKET_LIVE_TRADING_ENABLED
switch, explicit confirmation and compliance flags, environment-supplied
credentials, fill-or-kill order posting, REST rechecks immediately before entry,
per-session spend caps, per-bundle leg caps, and a process lock preventing two
live bots from trading the same wallet.
python -m pip install -e '.[live]'The full guarded workflow is documented in
docs/N_LEG_AND_LIVE_UPDATE.md.
| Document | Contents |
|---|---|
docs/ARCHITECTURE.md |
Runtime design, threading model, data flow diagram |
docs/CLI_AND_LIBRARY.md |
Full command reference and library usage |
docs/N_LEG_AND_LIVE_UPDATE.md |
N-leg packages and the guarded live executor |
docs/FORENSIC_AUDIT.md |
Forensic audit capture and profit-leakage reporting |
docs/KALSHI_SUPPORT.md |
Kalshi discovery and read-only scanning |
docs/WORLDCUP.md |
World Cup multi-market relation scanning |
monitoring/README.md |
Prometheus + Grafana setup and metric reference |
Contributions are welcome. Areas that are especially useful for this project:
- New venue adapters or order-book providers beyond Polymarket CLOB and Kalshi
- Arbitrage relations and strategies — additional N-leg packages, sizing, and exit logic
- Latency work on the hot path — the Rust WebSocket client, the Cython scan kernel, or the async scan loop
- Live-execution safety — risk controls, order handling, and reconciliation
- Observability — new Grafana panels or Prometheus metrics
- Market discovery — new candidate sources and pair-generation heuristics
- Documentation, tests, and reproducibility
The short version: check the open issues, fork, create a branch, add tests where
it makes sense, and open a pull request. For anything substantial — and for any
change to the live-trading path — open an issue first so the approach can be
discussed. Full details, including local setup and the PR checklist, are in
.github/CONTRIBUTING.md; all participation is
governed by the Code of Conduct. To report a
security issue, see .github/SECURITY.md.
Released under the MIT License.
This project is provided for research and educational purposes only. It is not financial, investment, or trading advice, and nothing here is a recommendation to trade any market. Prediction-market trading may be restricted or illegal in your jurisdiction; you are solely responsible for complying with all applicable laws and with each venue's terms of service. Live trading risks real financial loss. Use at your own risk.
