arbitrage — where we are paper mode

Plain-English status doc. Read before you run anything against real money.


1. What the status is

The bot is code-complete for Phase 1 (paper trading) and has a functional-but-unused skeleton for Phase 2 (live trading). It has never connected to the live Polymarket firehose yet, and it has never placed a real order. Everything that exists has been unit-tested against fake data — not yet validated against the real market.

PieceStateWhat that means
SQLite database layerdone Tables for events, outcomes, opportunities, baskets, fills, resolutions.
Gamma REST discoverydone Pulls the public list of Polymarket events and filters to the "negRisk" categoricals we can arb.
WebSocket L2 order bookdone, never live-tested Parses Polymarket's published message format. Hasn't shaken hands with the real server yet.
Opportunity engine (math)done For every event, walks the depth on every outcome, computes the best basket size, and reports the net edge after fees + gas.
Paper executordone Pretends to buy, models a 250 ms "someone beat me" penalty, records the outcome. No keys needed.
Live executorwired, gated off Will sign and submit orders through py-clob-client. Stays in dry-run mode unless you explicitly flip it.
Risk gatedone Hard caps on basket size, open-basket count per event, daily loss, and a kill-switch file.
Dashboarddone One HTML page, auto-refreshing. Shows opportunities, baskets, paper PnL, mode, and a kill button.
Resolution watchermanual only You can mark an event as resolved with arb resolve. There is no automatic poller yet — that's Phase 3.
Tests60 passing ~5 seconds to run. Core math is ≥90% covered. HTTP/socket code is thinner (~55%).
Bottom line: you can run it today in paper mode against live Polymarket data and watch what the engine would have done. You cannot yet point it at a funded wallet and walk away.

2. What it can do

The trade it looks for

Polymarket hosts "categorical" markets — say, "Who wins the 2028 US Election?" with one YES token per candidate. Because exactly one candidate wins, the fair prices of every YES token must sum to exactly $1. When someone in the order book is selling those YES tokens cheaply enough that the sum of best offers adds up to less than $1, you can buy one of each and hold a guaranteed $1 per basket. The gap between that sum and $1 is the profit.

What the bot does about it

  1. Discovers the categorical events on Polymarket.
  2. Subscribes to their live order books over a WebSocket.
  3. Watches every book update and re-runs the math.
  4. Sizes the trade honestly — it walks the depth on each leg, doesn't trust the top-of-book quote, and subtracts fees and the Polygon gas cost amortised across the basket.
  5. Logs every qualifying opportunity to SQLite.
  6. In paper mode: simulates the fills with a realistic latency penalty (250 ms by default, modelling "faster bots got there first"), records what it would have paid, and marks realised PnL when the event resolves.
  7. In live mode: submits FAK (fill-and-kill) orders on every leg in parallel, unwinds anything that partial-fills, and defers the on-chain redeem to a watcher you still have to build or run manually.
  8. Respects hard caps on basket size, total open baskets, daily loss, and a kill-switch file. It refuses to trade if any of those trip.
  9. Shows you what it's doing on a dashboard that refreshes itself every few seconds.

3. How to use it

First-time setup (paper mode, no keys)

cd C:\Users\matt\Dropbox\projects\arbitrage
.venv\Scripts\activate
pip install -e .
copy .env.example .env          # defaults are fine for paper mode
arb init                         # create the SQLite database

Run it

Open three terminals (or use a background process per step). In paper mode nothing you do here touches real money.

# Terminal 1 — keep the event list fresh
arb discover --loop

# Terminal 2 — start the scanner (WS + engine + paper executor)
arb scan

# Terminal 3 — dashboard
arb web
# open http://127.0.0.1:8000

Walk away for a few hours. Opportunities will log to the dashboard and into SQLite. Paper baskets will move from pending_resolutionredeemed (or invalid) as the underlying Polymarket events close out. At that point your paper realised-PnL column tells you whether the strategy would have been worth running.

Kill switch

Click the red kill button on the dashboard, or touch KILL in the project folder. The executor refuses to open any new baskets while that file exists.

Manually resolving an event (until the auto-watcher exists)

arb resolve <negRiskMarketID> --winner <winning_token_id>
arb resolve <negRiskMarketID> --winner invalid     # UMA voided it

Going live (not recommended yet — see §4)

Edit .env:

ARB_MODE=live
ARB_PRIVATE_KEY=0x...                  # dedicated wallet, small balance
ARB_FUNDER_ADDRESS=0x...               # same as wallet for EOA
ARB_MAX_BASKET_USD=25                  # keep it tiny for first real runs
ARB_MAX_OPEN_BASKETS=1

Even after you do this, the live executor defaults to dry_run=True in code — meaning it still logs what it would have signed rather than broadcasting. To actually broadcast orders you have to change the dry_run flag in arbitrage/cli.py. This is an intentional safety belt.

4. Will it work in the real world?

Honest answer: the code will run. Whether it will make money is an open empirical question — and the default expectation should be "no, not yet."

Why that's the default answer

What a realistic path forward looks like

  1. Paper-mode burn-in, one week. Run it continuously, watch the dashboard fill up, let events resolve. Total realised paper PnL at the end of that week is your headline number. If it's net negative after the latency penalty, don't even think about live mode.
  2. Investigate the misses. Every time the paper executor writes a failed basket (because depth vanished during the latency window) that's a trade a faster bot beat us to. Ratio of failed to redeemed is a proxy for how competitive the environment is.
  3. Tune or give up. If paper PnL is marginal, try a tighter minimum edge, larger basket sizes, only near-close events, or only low-volume events where the HFT bots don't bother. If it's still marginal, this strategy on this venue may simply be claimed.
  4. Only then, flip live with a tiny cap. $25 basket, one open at a time, one event. Expect slippage between paper and live PnL of at least 20–50% because the simulator can't model every micro-structural detail.

What would make me optimistic

Paper realised PnL of roughly +0.5% per basket, averaged across ≥50 baskets, with failure rate under a third. Below that, there isn't enough margin to survive the gap between simulated and live execution.

Above that, you still have to check that the opportunities it claims are scattered across many events (rather than concentrated in one lucky market that resolved favourably), and that the PnL is stable week-over-week rather than driven by one big win. Arbitrage should look boring and small on the balance curve — a straight line tilted gently up. If paper mode produces a hockey stick, something is wrong with the simulation and it will not replicate live.


Generated alongside commit for Phase 1 MVP, 60 passing tests, 71% total coverage (94% on the core math). Everything in this doc reflects the code as of this moment. If you change the strategy, the rebalancing of sections 1 and 2 is on you — section 4 will always age faster than the others.